Medusa Coverage Report GitHub

Files

    Files
    22
    Total Lines
    4662
    Coverage
    63.5%
    630 / 992 lines
    Actions
    55.6% lib/openzeppelin-contracts/contracts/access/Ownable.sol
    Lines covered: 5 / 9 (55.6%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
    3
    
                                                    
                                                
    4
    pragma solidity ^0.8.20;
    5
    
                                                    
                                                
    6
    import {Context} from "../utils/Context.sol";
    7
    
                                                    
                                                
    8
    /**
    9
     * @dev Contract module which provides a basic access control mechanism, where
    10
     * there is an account (an owner) that can be granted exclusive access to
    11
     * specific functions.
    12
     *
    13
     * The initial owner is set to the address provided by the deployer. This can
    14
     * later be changed with {transferOwnership}.
    15
     *
    16
     * This module is used through inheritance. It will make available the modifier
    17
     * `onlyOwner`, which can be applied to your functions to restrict their use to
    18
     * the owner.
    19
     */
    20
    abstract contract Ownable is Context {
    21
        address private _owner;
    22
    
                                                    
                                                
    23
        /**
    24
         * @dev The caller account is not authorized to perform an operation.
    25
         */
    26
        error OwnableUnauthorizedAccount(address account);
    27
    
                                                    
                                                
    28
        /**
    29
         * @dev The owner is not a valid owner account. (eg. `address(0)`)
    30
         */
    31
        error OwnableInvalidOwner(address owner);
    32
    
                                                    
                                                
    33
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    34
    
                                                    
                                                
    35
        /**
    36
         * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
    37
         */
    38
        constructor(address initialOwner) {
    39
    ✓ 1
            if (initialOwner == address(0)) {
    40
                revert OwnableInvalidOwner(address(0));
    41
            }
    42
            _transferOwnership(initialOwner);
    43
        }
    44
    
                                                    
                                                
    45
        /**
    46
         * @dev Throws if called by any account other than the owner.
    47
         */
    48
    ✓ 82.9K
        modifier onlyOwner() {
    49
            _checkOwner();
    50
            _;
    51
        }
    52
    
                                                    
                                                
    53
        /**
    54
         * @dev Returns the address of the current owner.
    55
         */
    56
        function owner() public view virtual returns (address) {
    57
    ✓ 135.5K
            return _owner;
    58
        }
    59
    
                                                    
                                                
    60
        /**
    61
         * @dev Throws if the sender is not the owner.
    62
         */
    63
        function _checkOwner() internal view virtual {
    64
    ✓ 135.5K
            if (owner() != _msgSender()) {
    65
                revert OwnableUnauthorizedAccount(_msgSender());
    66
            }
    67
        }
    68
    
                                                    
                                                
    69
        /**
    70
         * @dev Leaves the contract without owner. It will not be possible to call
    71
         * `onlyOwner` functions. Can only be called by the current owner.
    72
         *
    73
         * NOTE: Renouncing ownership will leave the contract without an owner,
    74
         * thereby disabling any functionality that is only available to the owner.
    75
         */
    76
        function renounceOwnership() public virtual onlyOwner {
    77
            _transferOwnership(address(0));
    78
        }
    79
    
                                                    
                                                
    80
        /**
    81
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
    82
         * Can only be called by the current owner.
    83
         */
    84
        function transferOwnership(address newOwner) public virtual onlyOwner {
    85
            if (newOwner == address(0)) {
    86
                revert OwnableInvalidOwner(address(0));
    87
            }
    88
            _transferOwnership(newOwner);
    89
        }
    90
    
                                                    
                                                
    91
        /**
    92
         * @dev Transfers ownership of the contract to a new account (`newOwner`).
    93
         * Internal function without access restriction.
    94
         */
    95
        function _transferOwnership(address newOwner) internal virtual {
    96
            address oldOwner = _owner;
    97
            _owner = newOwner;
    98
    ✓ 1
            emit OwnershipTransferred(oldOwner, newOwner);
    99
        }
    100
    }
    101
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/interfaces/IERC1363.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC1363.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.6.2;
    5
    
                                                    
                                                
    6
    import {IERC20} from "./IERC20.sol";
    7
    import {IERC165} from "./IERC165.sol";
    8
    
                                                    
                                                
    9
    /**
    10
     * @title IERC1363
    11
     * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
    12
     *
    13
     * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
    14
     * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
    15
     */
    16
    interface IERC1363 is IERC20, IERC165 {
    17
        /*
    18
         * Note: the ERC-165 identifier for this interface is 0xb0202a11.
    19
         * 0xb0202a11 ===
    20
         *   bytes4(keccak256('transferAndCall(address,uint256)')) ^
    21
         *   bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
    22
         *   bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
    23
         *   bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
    24
         *   bytes4(keccak256('approveAndCall(address,uint256)')) ^
    25
         *   bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
    26
         */
    27
    
                                                    
                                                
    28
        /**
    29
         * @dev Moves a `value` amount of tokens from the caller's account to `to`
    30
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
    31
         * @param to The address which you want to transfer to.
    32
         * @param value The amount of tokens to be transferred.
    33
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    34
         */
    35
        function transferAndCall(address to, uint256 value) external returns (bool);
    36
    
                                                    
                                                
    37
        /**
    38
         * @dev Moves a `value` amount of tokens from the caller's account to `to`
    39
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
    40
         * @param to The address which you want to transfer to.
    41
         * @param value The amount of tokens to be transferred.
    42
         * @param data Additional data with no specified format, sent in call to `to`.
    43
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    44
         */
    45
        function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
    46
    
                                                    
                                                
    47
        /**
    48
         * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
    49
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
    50
         * @param from The address which you want to send tokens from.
    51
         * @param to The address which you want to transfer to.
    52
         * @param value The amount of tokens to be transferred.
    53
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    54
         */
    55
        function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
    56
    
                                                    
                                                
    57
        /**
    58
         * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
    59
         * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
    60
         * @param from The address which you want to send tokens from.
    61
         * @param to The address which you want to transfer to.
    62
         * @param value The amount of tokens to be transferred.
    63
         * @param data Additional data with no specified format, sent in call to `to`.
    64
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    65
         */
    66
        function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
    67
    
                                                    
                                                
    68
        /**
    69
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
    70
         * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
    71
         * @param spender The address which will spend the funds.
    72
         * @param value The amount of tokens to be spent.
    73
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    74
         */
    75
        function approveAndCall(address spender, uint256 value) external returns (bool);
    76
    
                                                    
                                                
    77
        /**
    78
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
    79
         * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
    80
         * @param spender The address which will spend the funds.
    81
         * @param value The amount of tokens to be spent.
    82
         * @param data Additional data with no specified format, sent in call to `spender`.
    83
         * @return A boolean value indicating whether the operation succeeded unless throwing.
    84
         */
    85
        function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
    86
    }
    87
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/interfaces/IERC165.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC165.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.4.16;
    5
    
                                                    
                                                
    6
    import {IERC165} from "../utils/introspection/IERC165.sol";
    7
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC20.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.4.16;
    5
    
                                                    
                                                
    6
    import {IERC20} from "../token/ERC20/IERC20.sol";
    7
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.5.0) (interfaces/draft-IERC6093.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.8.4;
    5
    
                                                    
                                                
    6
    /**
    7
     * @dev Standard ERC-20 Errors
    8
     * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens.
    9
     */
    10
    interface IERC20Errors {
    11
        /**
    12
         * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
    13
         * @param sender Address whose tokens are being transferred.
    14
         * @param balance Current balance for the interacting account.
    15
         * @param needed Minimum amount required to perform a transfer.
    16
         */
    17
        error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
    18
    
                                                    
                                                
    19
        /**
    20
         * @dev Indicates a failure with the token `sender`. Used in transfers.
    21
         * @param sender Address whose tokens are being transferred.
    22
         */
    23
        error ERC20InvalidSender(address sender);
    24
    
                                                    
                                                
    25
        /**
    26
         * @dev Indicates a failure with the token `receiver`. Used in transfers.
    27
         * @param receiver Address to which tokens are being transferred.
    28
         */
    29
        error ERC20InvalidReceiver(address receiver);
    30
    
                                                    
                                                
    31
        /**
    32
         * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.
    33
         * @param spender Address that may be allowed to operate on tokens without being their owner.
    34
         * @param allowance Amount of tokens a `spender` is allowed to operate with.
    35
         * @param needed Minimum amount required to perform a transfer.
    36
         */
    37
        error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
    38
    
                                                    
                                                
    39
        /**
    40
         * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
    41
         * @param approver Address initiating an approval operation.
    42
         */
    43
        error ERC20InvalidApprover(address approver);
    44
    
                                                    
                                                
    45
        /**
    46
         * @dev Indicates a failure with the `spender` to be approved. Used in approvals.
    47
         * @param spender Address that may be allowed to operate on tokens without being their owner.
    48
         */
    49
        error ERC20InvalidSpender(address spender);
    50
    }
    51
    
                                                    
                                                
    52
    /**
    53
     * @dev Standard ERC-721 Errors
    54
     * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens.
    55
     */
    56
    interface IERC721Errors {
    57
        /**
    58
         * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-721.
    59
         * Used in balance queries.
    60
         * @param owner Address of the current owner of a token.
    61
         */
    62
        error ERC721InvalidOwner(address owner);
    63
    
                                                    
                                                
    64
        /**
    65
         * @dev Indicates a `tokenId` whose `owner` is the zero address.
    66
         * @param tokenId Identifier number of a token.
    67
         */
    68
        error ERC721NonexistentToken(uint256 tokenId);
    69
    
                                                    
                                                
    70
        /**
    71
         * @dev Indicates an error related to the ownership over a particular token. Used in transfers.
    72
         * @param sender Address whose tokens are being transferred.
    73
         * @param tokenId Identifier number of a token.
    74
         * @param owner Address of the current owner of a token.
    75
         */
    76
        error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);
    77
    
                                                    
                                                
    78
        /**
    79
         * @dev Indicates a failure with the token `sender`. Used in transfers.
    80
         * @param sender Address whose tokens are being transferred.
    81
         */
    82
        error ERC721InvalidSender(address sender);
    83
    
                                                    
                                                
    84
        /**
    85
         * @dev Indicates a failure with the token `receiver`. Used in transfers.
    86
         * @param receiver Address to which tokens are being transferred.
    87
         */
    88
        error ERC721InvalidReceiver(address receiver);
    89
    
                                                    
                                                
    90
        /**
    91
         * @dev Indicates a failure with the `operator`’s approval. Used in transfers.
    92
         * @param operator Address that may be allowed to operate on tokens without being their owner.
    93
         * @param tokenId Identifier number of a token.
    94
         */
    95
        error ERC721InsufficientApproval(address operator, uint256 tokenId);
    96
    
                                                    
                                                
    97
        /**
    98
         * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
    99
         * @param approver Address initiating an approval operation.
    100
         */
    101
        error ERC721InvalidApprover(address approver);
    102
    
                                                    
                                                
    103
        /**
    104
         * @dev Indicates a failure with the `operator` to be approved. Used in approvals.
    105
         * @param operator Address that may be allowed to operate on tokens without being their owner.
    106
         */
    107
        error ERC721InvalidOperator(address operator);
    108
    }
    109
    
                                                    
                                                
    110
    /**
    111
     * @dev Standard ERC-1155 Errors
    112
     * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens.
    113
     */
    114
    interface IERC1155Errors {
    115
        /**
    116
         * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
    117
         * @param sender Address whose tokens are being transferred.
    118
         * @param balance Current balance for the interacting account.
    119
         * @param needed Minimum amount required to perform a transfer.
    120
         * @param tokenId Identifier number of a token.
    121
         */
    122
        error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);
    123
    
                                                    
                                                
    124
        /**
    125
         * @dev Indicates a failure with the token `sender`. Used in transfers.
    126
         * @param sender Address whose tokens are being transferred.
    127
         */
    128
        error ERC1155InvalidSender(address sender);
    129
    
                                                    
                                                
    130
        /**
    131
         * @dev Indicates a failure with the token `receiver`. Used in transfers.
    132
         * @param receiver Address to which tokens are being transferred.
    133
         */
    134
        error ERC1155InvalidReceiver(address receiver);
    135
    
                                                    
                                                
    136
        /**
    137
         * @dev Indicates a failure with the `operator`’s approval. Used in transfers.
    138
         * @param operator Address that may be allowed to operate on tokens without being their owner.
    139
         * @param owner Address of the current owner of a token.
    140
         */
    141
        error ERC1155MissingApprovalForAll(address operator, address owner);
    142
    
                                                    
                                                
    143
        /**
    144
         * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
    145
         * @param approver Address initiating an approval operation.
    146
         */
    147
        error ERC1155InvalidApprover(address approver);
    148
    
                                                    
                                                
    149
        /**
    150
         * @dev Indicates a failure with the `operator` to be approved. Used in approvals.
    151
         * @param operator Address that may be allowed to operate on tokens without being their owner.
    152
         */
    153
        error ERC1155InvalidOperator(address operator);
    154
    
                                                    
                                                
    155
        /**
    156
         * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.
    157
         * Used in batch transfers.
    158
         * @param idsLength Length of the array of token identifiers
    159
         * @param valuesLength Length of the array of token amounts
    160
         */
    161
        error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);
    162
    }
    163
    
                                                    
                                                
    100.0% lib/openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol
    Lines covered: 1 / 1 (100.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.20;
    3
    
                                                    
                                                
    4
    import {ERC20} from "../../token/ERC20/ERC20.sol";
    5
    
                                                    
                                                
    6
    ✓ 239.3K
    contract ERC20Mock is ERC20 {
    7
        constructor() ERC20("ERC20Mock", "E20M") {}
    8
    
                                                    
                                                
    9
        function mint(address account, uint256 amount) external {
    10
            _mint(account, amount);
    11
        }
    12
    
                                                    
                                                
    13
        function burn(address account, uint256 amount) external {
    14
            _burn(account, amount);
    15
        }
    16
    }
    17
    
                                                    
                                                
    56.0% lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol
    Lines covered: 14 / 25 (56.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.5.0) (token/ERC20/ERC20.sol)
    3
    
                                                    
                                                
    4
    pragma solidity ^0.8.20;
    5
    
                                                    
                                                
    6
    import {IERC20} from "./IERC20.sol";
    7
    import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
    8
    import {Context} from "../../utils/Context.sol";
    9
    import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
    10
    
                                                    
                                                
    11
    /**
    12
     * @dev Implementation of the {IERC20} interface.
    13
     *
    14
     * This implementation is agnostic to the way tokens are created. This means
    15
     * that a supply mechanism has to be added in a derived contract using {_mint}.
    16
     *
    17
     * TIP: For a detailed writeup see our guide
    18
     * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
    19
     * to implement supply mechanisms].
    20
     *
    21
     * The default value of {decimals} is 18. To change this, you should override
    22
     * this function so it returns a different value.
    23
     *
    24
     * We have followed general OpenZeppelin Contracts guidelines: functions revert
    25
     * instead returning `false` on failure. This behavior is nonetheless
    26
     * conventional and does not conflict with the expectations of ERC-20
    27
     * applications.
    28
     */
    29
    abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
    30
        mapping(address account => uint256) private _balances;
    31
    
                                                    
                                                
    32
        mapping(address account => mapping(address spender => uint256)) private _allowances;
    33
    
                                                    
                                                
    34
        uint256 private _totalSupply;
    35
    
                                                    
                                                
    36
        string private _name;
    37
        string private _symbol;
    38
    
                                                    
                                                
    39
        /**
    40
         * @dev Sets the values for {name} and {symbol}.
    41
         *
    42
         * Both values are immutable: they can only be set once during construction.
    43
         */
    44
        constructor(string memory name_, string memory symbol_) {
    45
    ✓ 1
            _name = name_;
    46
            _symbol = symbol_;
    47
        }
    48
    
                                                    
                                                
    49
        /**
    50
         * @dev Returns the name of the token.
    51
         */
    52
        function name() public view virtual returns (string memory) {
    53
            return _name;
    54
        }
    55
    
                                                    
                                                
    56
        /**
    57
         * @dev Returns the symbol of the token, usually a shorter version of the
    58
         * name.
    59
         */
    60
        function symbol() public view virtual returns (string memory) {
    61
            return _symbol;
    62
        }
    63
    
                                                    
                                                
    64
        /**
    65
         * @dev Returns the number of decimals used to get its user representation.
    66
         * For example, if `decimals` equals `2`, a balance of `505` tokens should
    67
         * be displayed to a user as `5.05` (`505 / 10 ** 2`).
    68
         *
    69
         * Tokens usually opt for a value of 18, imitating the relationship between
    70
         * Ether and Wei. This is the default value returned by this function, unless
    71
         * it's overridden.
    72
         *
    73
         * NOTE: This information is only used for _display_ purposes: it in
    74
         * no way affects any of the arithmetic of the contract, including
    75
         * {IERC20-balanceOf} and {IERC20-transfer}.
    76
         */
    77
        function decimals() public view virtual returns (uint8) {
    78
            return 18;
    79
        }
    80
    
                                                    
                                                
    81
        /// @inheritdoc IERC20
    82
        function totalSupply() public view virtual returns (uint256) {
    83
            return _totalSupply;
    84
        }
    85
    
                                                    
                                                
    86
        /// @inheritdoc IERC20
    87
        function balanceOf(address account) public view virtual returns (uint256) {
    88
            return _balances[account];
    89
        }
    90
    
                                                    
                                                
    91
        /**
    92
         * @dev See {IERC20-transfer}.
    93
         *
    94
         * Requirements:
    95
         *
    96
         * - `to` cannot be the zero address.
    97
         * - the caller must have a balance of at least `value`.
    98
         */
    99
        function transfer(address to, uint256 value) public virtual returns (bool) {
    100
            address owner = _msgSender();
    101
    ✓ 154.9K
            _transfer(owner, to, value);
    102
            return true;
    103
        }
    104
    
                                                    
                                                
    105
        /// @inheritdoc IERC20
    106
        function allowance(address owner, address spender) public view virtual returns (uint256) {
    107
            return _allowances[owner][spender];
    108
        }
    109
    
                                                    
                                                
    110
        /**
    111
         * @dev See {IERC20-approve}.
    112
         *
    113
         * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
    114
         * `transferFrom`. This is semantically equivalent to an infinite approval.
    115
         *
    116
         * Requirements:
    117
         *
    118
         * - `spender` cannot be the zero address.
    119
         */
    120
        function approve(address spender, uint256 value) public virtual returns (bool) {
    121
            address owner = _msgSender();
    122
            _approve(owner, spender, value);
    123
            return true;
    124
        }
    125
    
                                                    
                                                
    126
        /**
    127
         * @dev See {IERC20-transferFrom}.
    128
         *
    129
         * Skips emitting an {Approval} event indicating an allowance update. This is not
    130
         * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
    131
         *
    132
         * NOTE: Does not update the allowance if the current allowance
    133
         * is the maximum `uint256`.
    134
         *
    135
         * Requirements:
    136
         *
    137
         * - `from` and `to` cannot be the zero address.
    138
         * - `from` must have a balance of at least `value`.
    139
         * - the caller must have allowance for ``from``'s tokens of at least
    140
         * `value`.
    141
         */
    142
        function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
    143
            address spender = _msgSender();
    144
            _spendAllowance(from, spender, value);
    145
    ✓ 84.3K
            _transfer(from, to, value);
    146
            return true;
    147
        }
    148
    
                                                    
                                                
    149
        /**
    150
         * @dev Moves a `value` amount of tokens from `from` to `to`.
    151
         *
    152
         * This internal function is equivalent to {transfer}, and can be used to
    153
         * e.g. implement automatic token fees, slashing mechanisms, etc.
    154
         *
    155
         * Emits a {Transfer} event.
    156
         *
    157
         * NOTE: This function is not virtual, {_update} should be overridden instead.
    158
         */
    159
        function _transfer(address from, address to, uint256 value) internal {
    160
    ✓ 154.9K
            if (from == address(0)) {
    161
                revert ERC20InvalidSender(address(0));
    162
            }
    163
    ✓ 154.9K
            if (to == address(0)) {
    164
                revert ERC20InvalidReceiver(address(0));
    165
            }
    166
            _update(from, to, value);
    167
        }
    168
    
                                                    
                                                
    169
        /**
    170
         * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
    171
         * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
    172
         * this function.
    173
         *
    174
         * Emits a {Transfer} event.
    175
         */
    176
        function _update(address from, address to, uint256 value) internal virtual {
    177
            if (from == address(0)) {
    178
                // Overflow check required: The rest of the code assumes that totalSupply never overflows
    179
    ✓ 113
                _totalSupply += value;
    180
            } else {
    181
                uint256 fromBalance = _balances[from];
    182
    ✓ 154.9K
                if (fromBalance < value) {
    183
                    revert ERC20InsufficientBalance(from, fromBalance, value);
    184
                }
    185
                unchecked {
    186
                    // Overflow not possible: value <= fromBalance <= totalSupply.
    187
                    _balances[from] = fromBalance - value;
    188
                }
    189
            }
    190
    
                                                    
                                                
    191
            if (to == address(0)) {
    192
                unchecked {
    193
                    // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
    194
                    _totalSupply -= value;
    195
                }
    196
            } else {
    197
                unchecked {
    198
                    // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
    199
                    _balances[to] += value;
    200
                }
    201
            }
    202
    
                                                    
                                                
    203
    ✓ 154.9K
            emit Transfer(from, to, value);
    204
        }
    205
    
                                                    
                                                
    206
        /**
    207
         * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
    208
         * Relies on the `_update` mechanism
    209
         *
    210
         * Emits a {Transfer} event with `from` set to the zero address.
    211
         *
    212
         * NOTE: This function is not virtual, {_update} should be overridden instead.
    213
         */
    214
        function _mint(address account, uint256 value) internal {
    215
    ✓ 113
            if (account == address(0)) {
    216
                revert ERC20InvalidReceiver(address(0));
    217
            }
    218
            _update(address(0), account, value);
    219
        }
    220
    
                                                    
                                                
    221
        /**
    222
         * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
    223
         * Relies on the `_update` mechanism.
    224
         *
    225
         * Emits a {Transfer} event with `to` set to the zero address.
    226
         *
    227
         * NOTE: This function is not virtual, {_update} should be overridden instead
    228
         */
    229
        function _burn(address account, uint256 value) internal {
    230
            if (account == address(0)) {
    231
                revert ERC20InvalidSender(address(0));
    232
            }
    233
            _update(account, address(0), value);
    234
        }
    235
    
                                                    
                                                
    236
        /**
    237
         * @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.
    238
         *
    239
         * This internal function is equivalent to `approve`, and can be used to
    240
         * e.g. set automatic allowances for certain subsystems, etc.
    241
         *
    242
         * Emits an {Approval} event.
    243
         *
    244
         * Requirements:
    245
         *
    246
         * - `owner` cannot be the zero address.
    247
         * - `spender` cannot be the zero address.
    248
         *
    249
         * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
    250
         */
    251
        function _approve(address owner, address spender, uint256 value) internal {
    252
            _approve(owner, spender, value, true);
    253
        }
    254
    
                                                    
                                                
    255
        /**
    256
         * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
    257
         *
    258
         * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
    259
         * `_spendAllowance` during the `transferFrom` operation sets the flag to false. This saves gas by not emitting any
    260
         * `Approval` event during `transferFrom` operations.
    261
         *
    262
         * Anyone who wishes to continue emitting `Approval` events on the `transferFrom` operation can force the flag to
    263
         * true using the following override:
    264
         *
    265
         * ```solidity
    266
         * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
    267
         *     super._approve(owner, spender, value, true);
    268
         * }
    269
         * ```
    270
         *
    271
         * Requirements are the same as {_approve}.
    272
         */
    273
        function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
    274
    ✓ 84.3K
            if (owner == address(0)) {
    275
                revert ERC20InvalidApprover(address(0));
    276
            }
    277
    ✓ 84.3K
            if (spender == address(0)) {
    278
                revert ERC20InvalidSpender(address(0));
    279
            }
    280
            _allowances[owner][spender] = value;
    281
            if (emitEvent) {
    282
    ✓ 84.3K
                emit Approval(owner, spender, value);
    283
            }
    284
        }
    285
    
                                                    
                                                
    286
        /**
    287
         * @dev Updates `owner`'s allowance for `spender` based on spent `value`.
    288
         *
    289
         * Does not update the allowance value in case of infinite allowance.
    290
         * Revert if not enough allowance is available.
    291
         *
    292
         * Does not emit an {Approval} event.
    293
         */
    294
        function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
    295
            uint256 currentAllowance = allowance(owner, spender);
    296
    ✓ 84.3K
            if (currentAllowance < type(uint256).max) {
    297
    ✓ 84.3K
                if (currentAllowance < value) {
    298
                    revert ERC20InsufficientAllowance(spender, currentAllowance, value);
    299
                }
    300
                unchecked {
    301
                    _approve(owner, spender, currentAllowance - value, false);
    302
                }
    303
            }
    304
        }
    305
    }
    306
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.4.16;
    5
    
                                                    
                                                
    6
    /**
    7
     * @dev Interface of the ERC-20 standard as defined in the ERC.
    8
     */
    9
    interface IERC20 {
    10
        /**
    11
         * @dev Emitted when `value` tokens are moved from one account (`from`) to
    12
         * another (`to`).
    13
         *
    14
         * Note that `value` may be zero.
    15
         */
    16
        event Transfer(address indexed from, address indexed to, uint256 value);
    17
    
                                                    
                                                
    18
        /**
    19
         * @dev Emitted when the allowance of a `spender` for an `owner` is set by
    20
         * a call to {approve}. `value` is the new allowance.
    21
         */
    22
        event Approval(address indexed owner, address indexed spender, uint256 value);
    23
    
                                                    
                                                
    24
        /**
    25
         * @dev Returns the value of tokens in existence.
    26
         */
    27
        function totalSupply() external view returns (uint256);
    28
    
                                                    
                                                
    29
        /**
    30
         * @dev Returns the value of tokens owned by `account`.
    31
         */
    32
        function balanceOf(address account) external view returns (uint256);
    33
    
                                                    
                                                
    34
        /**
    35
         * @dev Moves a `value` amount of tokens from the caller's account to `to`.
    36
         *
    37
         * Returns a boolean value indicating whether the operation succeeded.
    38
         *
    39
         * Emits a {Transfer} event.
    40
         */
    41
        function transfer(address to, uint256 value) external returns (bool);
    42
    
                                                    
                                                
    43
        /**
    44
         * @dev Returns the remaining number of tokens that `spender` will be
    45
         * allowed to spend on behalf of `owner` through {transferFrom}. This is
    46
         * zero by default.
    47
         *
    48
         * This value changes when {approve} or {transferFrom} are called.
    49
         */
    50
        function allowance(address owner, address spender) external view returns (uint256);
    51
    
                                                    
                                                
    52
        /**
    53
         * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
    54
         * caller's tokens.
    55
         *
    56
         * Returns a boolean value indicating whether the operation succeeded.
    57
         *
    58
         * IMPORTANT: Beware that changing an allowance with this method brings the risk
    59
         * that someone may use both the old and the new allowance by unfortunate
    60
         * transaction ordering. One possible solution to mitigate this race
    61
         * condition is to first reduce the spender's allowance to 0 and set the
    62
         * desired value afterwards:
    63
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
    64
         *
    65
         * Emits an {Approval} event.
    66
         */
    67
        function approve(address spender, uint256 value) external returns (bool);
    68
    
                                                    
                                                
    69
        /**
    70
         * @dev Moves a `value` amount of tokens from `from` to `to` using the
    71
         * allowance mechanism. `value` is then deducted from the caller's
    72
         * allowance.
    73
         *
    74
         * Returns a boolean value indicating whether the operation succeeded.
    75
         *
    76
         * Emits a {Transfer} event.
    77
         */
    78
        function transferFrom(address from, address to, uint256 value) external returns (bool);
    79
    }
    80
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/IERC20Metadata.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.6.2;
    5
    
                                                    
                                                
    6
    import {IERC20} from "../IERC20.sol";
    7
    
                                                    
                                                
    8
    /**
    9
     * @dev Interface for the optional metadata functions from the ERC-20 standard.
    10
     */
    11
    interface IERC20Metadata is IERC20 {
    12
        /**
    13
         * @dev Returns the name of the token.
    14
         */
    15
        function name() external view returns (string memory);
    16
    
                                                    
                                                
    17
        /**
    18
         * @dev Returns the symbol of the token.
    19
         */
    20
        function symbol() external view returns (string memory);
    21
    
                                                    
                                                
    22
        /**
    23
         * @dev Returns the decimals places of the token.
    24
         */
    25
        function decimals() external view returns (uint8);
    26
    }
    27
    
                                                    
                                                
    75.0% lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
    Lines covered: 6 / 8 (75.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.5.0) (token/ERC20/utils/SafeERC20.sol)
    3
    
                                                    
                                                
    4
    pragma solidity ^0.8.20;
    5
    
                                                    
                                                
    6
    import {IERC20} from "../IERC20.sol";
    7
    import {IERC1363} from "../../../interfaces/IERC1363.sol";
    8
    
                                                    
                                                
    9
    /**
    10
     * @title SafeERC20
    11
     * @dev Wrappers around ERC-20 operations that throw on failure (when the token
    12
     * contract returns false). Tokens that return no value (and instead revert or
    13
     * throw on failure) are also supported, non-reverting calls are assumed to be
    14
     * successful.
    15
     * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
    16
     * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
    17
     */
    18
    ✓ 2
    library SafeERC20 {
    19
        /**
    20
         * @dev An operation with an ERC-20 token failed.
    21
         */
    22
        error SafeERC20FailedOperation(address token);
    23
    
                                                    
                                                
    24
        /**
    25
         * @dev Indicates a failed `decreaseAllowance` request.
    26
         */
    27
        error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
    28
    
                                                    
                                                
    29
        /**
    30
         * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
    31
         * non-reverting calls are assumed to be successful.
    32
         */
    33
        function safeTransfer(IERC20 token, address to, uint256 value) internal {
    34
    ✓ 70.6K
            if (!_safeTransfer(token, to, value, true)) {
    35
                revert SafeERC20FailedOperation(address(token));
    36
            }
    37
        }
    38
    
                                                    
                                                
    39
        /**
    40
         * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
    41
         * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
    42
         */
    43
        function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
    44
    ✓ 67.1K
            if (!_safeTransferFrom(token, from, to, value, true)) {
    45
                revert SafeERC20FailedOperation(address(token));
    46
            }
    47
        }
    48
    
                                                    
                                                
    49
        /**
    50
         * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
    51
         */
    52
        function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
    53
            return _safeTransfer(token, to, value, false);
    54
        }
    55
    
                                                    
                                                
    56
        /**
    57
         * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
    58
         */
    59
        function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
    60
            return _safeTransferFrom(token, from, to, value, false);
    61
        }
    62
    
                                                    
                                                
    63
        /**
    64
         * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
    65
         * non-reverting calls are assumed to be successful.
    66
         *
    67
         * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
    68
         * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
    69
         * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
    70
         * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
    71
         */
    72
        function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
    73
            uint256 oldAllowance = token.allowance(address(this), spender);
    74
            forceApprove(token, spender, oldAllowance + value);
    75
        }
    76
    
                                                    
                                                
    77
        /**
    78
         * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
    79
         * value, non-reverting calls are assumed to be successful.
    80
         *
    81
         * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
    82
         * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
    83
         * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
    84
         * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
    85
         */
    86
        function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
    87
            unchecked {
    88
                uint256 currentAllowance = token.allowance(address(this), spender);
    89
                if (currentAllowance < requestedDecrease) {
    90
                    revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
    91
                }
    92
                forceApprove(token, spender, currentAllowance - requestedDecrease);
    93
            }
    94
        }
    95
    
                                                    
                                                
    96
        /**
    97
         * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
    98
         * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
    99
         * to be set to zero before setting it to a non-zero value, such as USDT.
    100
         *
    101
         * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
    102
         * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
    103
         * set here.
    104
         */
    105
        function forceApprove(IERC20 token, address spender, uint256 value) internal {
    106
            if (!_safeApprove(token, spender, value, false)) {
    107
                if (!_safeApprove(token, spender, 0, true)) revert SafeERC20FailedOperation(address(token));
    108
                if (!_safeApprove(token, spender, value, true)) revert SafeERC20FailedOperation(address(token));
    109
            }
    110
        }
    111
    
                                                    
                                                
    112
        /**
    113
         * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
    114
         * code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
    115
         * targeting contracts.
    116
         *
    117
         * Reverts if the returned value is other than `true`.
    118
         */
    119
        function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
    120
            if (to.code.length == 0) {
    121
                safeTransfer(token, to, value);
    122
            } else if (!token.transferAndCall(to, value, data)) {
    123
                revert SafeERC20FailedOperation(address(token));
    124
            }
    125
        }
    126
    
                                                    
                                                
    127
        /**
    128
         * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
    129
         * has no code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
    130
         * targeting contracts.
    131
         *
    132
         * Reverts if the returned value is other than `true`.
    133
         */
    134
        function transferFromAndCallRelaxed(
    135
            IERC1363 token,
    136
            address from,
    137
            address to,
    138
            uint256 value,
    139
            bytes memory data
    140
        ) internal {
    141
            if (to.code.length == 0) {
    142
                safeTransferFrom(token, from, to, value);
    143
            } else if (!token.transferFromAndCall(from, to, value, data)) {
    144
                revert SafeERC20FailedOperation(address(token));
    145
            }
    146
        }
    147
    
                                                    
                                                
    148
        /**
    149
         * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
    150
         * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
    151
         * targeting contracts.
    152
         *
    153
         * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
    154
         * Oppositely, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
    155
         * once without retrying, and relies on the returned value to be true.
    156
         *
    157
         * Reverts if the returned value is other than `true`.
    158
         */
    159
        function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
    160
            if (to.code.length == 0) {
    161
                forceApprove(token, to, value);
    162
            } else if (!token.approveAndCall(to, value, data)) {
    163
                revert SafeERC20FailedOperation(address(token));
    164
            }
    165
        }
    166
    
                                                    
                                                
    167
        /**
    168
         * @dev Imitates a Solidity `token.transfer(to, value)` call, relaxing the requirement on the return value: the
    169
         * return value is optional (but if data is returned, it must not be false).
    170
         *
    171
         * @param token The token targeted by the call.
    172
         * @param to The recipient of the tokens
    173
         * @param value The amount of token to transfer
    174
         * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean.
    175
         */
    176
        function _safeTransfer(IERC20 token, address to, uint256 value, bool bubble) private returns (bool success) {
    177
    ✓ 70.6K
            bytes4 selector = IERC20.transfer.selector;
    178
    
                                                    
                                                
    179
    ✓ 70.6K
            assembly ("memory-safe") {
    180
                let fmp := mload(0x40)
    181
                mstore(0x00, selector)
    182
                mstore(0x04, and(to, shr(96, not(0))))
    183
                mstore(0x24, value)
    184
                success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)
    185
                // if call success and return is true, all is good.
    186
                // otherwise (not success or return is not true), we need to perform further checks
    187
                if iszero(and(success, eq(mload(0x00), 1))) {
    188
                    // if the call was a failure and bubble is enabled, bubble the error
    189
                    if and(iszero(success), bubble) {
    190
                        returndatacopy(fmp, 0x00, returndatasize())
    191
                        revert(fmp, returndatasize())
    192
                    }
    193
                    // if the return value is not true, then the call is only successful if:
    194
                    // - the token address has code
    195
                    // - the returndata is empty
    196
                    success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0)))
    197
                }
    198
                mstore(0x40, fmp)
    199
            }
    200
        }
    201
    
                                                    
                                                
    202
        /**
    203
         * @dev Imitates a Solidity `token.transferFrom(from, to, value)` call, relaxing the requirement on the return
    204
         * value: the return value is optional (but if data is returned, it must not be false).
    205
         *
    206
         * @param token The token targeted by the call.
    207
         * @param from The sender of the tokens
    208
         * @param to The recipient of the tokens
    209
         * @param value The amount of token to transfer
    210
         * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean.
    211
         */
    212
        function _safeTransferFrom(
    213
            IERC20 token,
    214
            address from,
    215
            address to,
    216
            uint256 value,
    217
            bool bubble
    218
        ) private returns (bool success) {
    219
            bytes4 selector = IERC20.transferFrom.selector;
    220
    
                                                    
                                                
    221
    ✓ 67.1K
            assembly ("memory-safe") {
    222
                let fmp := mload(0x40)
    223
                mstore(0x00, selector)
    224
                mstore(0x04, and(from, shr(96, not(0))))
    225
                mstore(0x24, and(to, shr(96, not(0))))
    226
                mstore(0x44, value)
    227
                success := call(gas(), token, 0, 0x00, 0x64, 0x00, 0x20)
    228
                // if call success and return is true, all is good.
    229
                // otherwise (not success or return is not true), we need to perform further checks
    230
                if iszero(and(success, eq(mload(0x00), 1))) {
    231
                    // if the call was a failure and bubble is enabled, bubble the error
    232
                    if and(iszero(success), bubble) {
    233
                        returndatacopy(fmp, 0x00, returndatasize())
    234
                        revert(fmp, returndatasize())
    235
                    }
    236
                    // if the return value is not true, then the call is only successful if:
    237
                    // - the token address has code
    238
                    // - the returndata is empty
    239
                    success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0)))
    240
                }
    241
                mstore(0x40, fmp)
    242
                mstore(0x60, 0)
    243
            }
    244
        }
    245
    
                                                    
                                                
    246
        /**
    247
         * @dev Imitates a Solidity `token.approve(spender, value)` call, relaxing the requirement on the return value:
    248
         * the return value is optional (but if data is returned, it must not be false).
    249
         *
    250
         * @param token The token targeted by the call.
    251
         * @param spender The spender of the tokens
    252
         * @param value The amount of token to transfer
    253
         * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean.
    254
         */
    255
        function _safeApprove(IERC20 token, address spender, uint256 value, bool bubble) private returns (bool success) {
    256
            bytes4 selector = IERC20.approve.selector;
    257
    
                                                    
                                                
    258
            assembly ("memory-safe") {
    259
                let fmp := mload(0x40)
    260
                mstore(0x00, selector)
    261
                mstore(0x04, and(spender, shr(96, not(0))))
    262
                mstore(0x24, value)
    263
                success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20)
    264
                // if call success and return is true, all is good.
    265
                // otherwise (not success or return is not true), we need to perform further checks
    266
                if iszero(and(success, eq(mload(0x00), 1))) {
    267
                    // if the call was a failure and bubble is enabled, bubble the error
    268
                    if and(iszero(success), bubble) {
    269
                        returndatacopy(fmp, 0x00, returndatasize())
    270
                        revert(fmp, returndatasize())
    271
                    }
    272
                    // if the return value is not true, then the call is only successful if:
    273
                    // - the token address has code
    274
                    // - the returndata is empty
    275
                    success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0)))
    276
                }
    277
                mstore(0x40, fmp)
    278
            }
    279
        }
    280
    }
    281
    
                                                    
                                                
    100.0% lib/openzeppelin-contracts/contracts/utils/Context.sol
    Lines covered: 1 / 1 (100.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
    3
    
                                                    
                                                
    4
    pragma solidity ^0.8.20;
    5
    
                                                    
                                                
    6
    /**
    7
     * @dev Provides information about the current execution context, including the
    8
     * sender of the transaction and its data. While these are generally available
    9
     * via msg.sender and msg.data, they should not be accessed in such a direct
    10
     * manner, since when dealing with meta-transactions the account sending and
    11
     * paying for execution may not be the actual sender (as far as an application
    12
     * is concerned).
    13
     *
    14
     * This contract is only required for intermediate, library-like contracts.
    15
     */
    16
    abstract contract Context {
    17
        function _msgSender() internal view virtual returns (address) {
    18
    ✓ 135.5K
            return msg.sender;
    19
        }
    20
    
                                                    
                                                
    21
        function _msgData() internal view virtual returns (bytes calldata) {
    22
            return msg.data;
    23
        }
    24
    
                                                    
                                                
    25
        function _contextSuffixLength() internal view virtual returns (uint256) {
    26
            return 0;
    27
        }
    28
    }
    29
    
                                                    
                                                
    83.3% lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol
    Lines covered: 5 / 6 (83.3%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.5.0) (utils/ReentrancyGuard.sol)
    3
    
                                                    
                                                
    4
    pragma solidity ^0.8.20;
    5
    
                                                    
                                                
    6
    import {StorageSlot} from "./StorageSlot.sol";
    7
    
                                                    
                                                
    8
    /**
    9
     * @dev Contract module that helps prevent reentrant calls to a function.
    10
     *
    11
     * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
    12
     * available, which can be applied to functions to make sure there are no nested
    13
     * (reentrant) calls to them.
    14
     *
    15
     * Note that because there is a single `nonReentrant` guard, functions marked as
    16
     * `nonReentrant` may not call one another. This can be worked around by making
    17
     * those functions `private`, and then adding `external` `nonReentrant` entry
    18
     * points to them.
    19
     *
    20
     * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
    21
     * consider using {ReentrancyGuardTransient} instead.
    22
     *
    23
     * TIP: If you would like to learn more about reentrancy and alternative ways
    24
     * to protect against it, check out our blog post
    25
     * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
    26
     *
    27
     * IMPORTANT: Deprecated. This storage-based reentrancy guard will be removed and replaced
    28
     * by the {ReentrancyGuardTransient} variant in v6.0.
    29
     *
    30
     * @custom:stateless
    31
     */
    32
    abstract contract ReentrancyGuard {
    33
        using StorageSlot for bytes32;
    34
    
                                                    
                                                
    35
        // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
    36
        bytes32 private constant REENTRANCY_GUARD_STORAGE =
    37
    ✓ 1
            0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
    38
    
                                                    
                                                
    39
        // Booleans are more expensive than uint256 or any type that takes up a full
    40
        // word because each write operation emits an extra SLOAD to first read the
    41
        // slot's contents, replace the bits taken up by the boolean, and then write
    42
        // back. This is the compiler's defense against contract upgrades and
    43
        // pointer aliasing, and it cannot be disabled.
    44
    
                                                    
                                                
    45
        // The values being non-zero value makes deployment a bit more expensive,
    46
        // but in exchange the refund on every call to nonReentrant will be lower in
    47
        // amount. Since refunds are capped to a percentage of the total
    48
        // transaction's gas, it is best to keep them low in cases like this one, to
    49
        // increase the likelihood of the full refund coming into effect.
    50
        uint256 private constant NOT_ENTERED = 1;
    51
    ✓ 52.5K
        uint256 private constant ENTERED = 2;
    52
    
                                                    
                                                
    53
        /**
    54
         * @dev Unauthorized reentrant call.
    55
         */
    56
        error ReentrancyGuardReentrantCall();
    57
    
                                                    
                                                
    58
        constructor() {
    59
            _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED;
    60
        }
    61
    
                                                    
                                                
    62
        /**
    63
         * @dev Prevents a contract from calling itself, directly or indirectly.
    64
         * Calling a `nonReentrant` function from another `nonReentrant`
    65
         * function is not supported. It is possible to prevent this from happening
    66
         * by making the `nonReentrant` function external, and making it call a
    67
         * `private` function that does the actual work.
    68
         */
    69
    ✓ 35.3K
        modifier nonReentrant() {
    70
            _nonReentrantBefore();
    71
            _;
    72
            _nonReentrantAfter();
    73
        }
    74
    
                                                    
                                                
    75
        /**
    76
         * @dev A `view` only version of {nonReentrant}. Use to block view functions
    77
         * from being called, preventing reading from inconsistent contract state.
    78
         *
    79
         * CAUTION: This is a "view" modifier and does not change the reentrancy
    80
         * status. Use it only on view functions. For payable or non-payable functions,
    81
         * use the standard {nonReentrant} modifier instead.
    82
         */
    83
        modifier nonReentrantView() {
    84
            _nonReentrantBeforeView();
    85
            _;
    86
        }
    87
    
                                                    
                                                
    88
        function _nonReentrantBeforeView() private view {
    89
            if (_reentrancyGuardEntered()) {
    90
                revert ReentrancyGuardReentrantCall();
    91
            }
    92
        }
    93
    
                                                    
                                                
    94
    ✓ 52.5K
        function _nonReentrantBefore() private {
    95
            // On the first call to nonReentrant, _status will be NOT_ENTERED
    96
            _nonReentrantBeforeView();
    97
    
                                                    
                                                
    98
            // Any calls to nonReentrant after this point will fail
    99
            _reentrancyGuardStorageSlot().getUint256Slot().value = ENTERED;
    100
        }
    101
    
                                                    
                                                
    102
        function _nonReentrantAfter() private {
    103
            // By storing the original value once again, a refund is triggered (see
    104
            // https://eips.ethereum.org/EIPS/eip-2200)
    105
            _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED;
    106
        }
    107
    
                                                    
                                                
    108
        /**
    109
         * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
    110
         * `nonReentrant` function in the call stack.
    111
         */
    112
        function _reentrancyGuardEntered() internal view returns (bool) {
    113
    ✓ 52.5K
            return _reentrancyGuardStorageSlot().getUint256Slot().value == ENTERED;
    114
        }
    115
    
                                                    
                                                
    116
        function _reentrancyGuardStorageSlot() internal pure virtual returns (bytes32) {
    117
            return REENTRANCY_GUARD_STORAGE;
    118
        }
    119
    }
    120
    
                                                    
                                                
    100.0% lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
    Lines covered: 1 / 1 (100.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)
    3
    // This file was procedurally generated from scripts/generate/templates/StorageSlot.js.
    4
    
                                                    
                                                
    5
    pragma solidity ^0.8.20;
    6
    
                                                    
                                                
    7
    /**
    8
     * @dev Library for reading and writing primitive types to specific storage slots.
    9
     *
    10
     * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
    11
     * This library helps with reading and writing to such slots without the need for inline assembly.
    12
     *
    13
     * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
    14
     *
    15
     * Example usage to set ERC-1967 implementation slot:
    16
     * ```solidity
    17
     * contract ERC1967 {
    18
     *     // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
    19
     *     bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    20
     *
    21
     *     function _getImplementation() internal view returns (address) {
    22
     *         return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    23
     *     }
    24
     *
    25
     *     function _setImplementation(address newImplementation) internal {
    26
     *         require(newImplementation.code.length > 0);
    27
     *         StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    28
     *     }
    29
     * }
    30
     * ```
    31
     *
    32
     * TIP: Consider using this library along with {SlotDerivation}.
    33
     */
    34
    ✓ 2
    library StorageSlot {
    35
        struct AddressSlot {
    36
            address value;
    37
        }
    38
    
                                                    
                                                
    39
        struct BooleanSlot {
    40
            bool value;
    41
        }
    42
    
                                                    
                                                
    43
        struct Bytes32Slot {
    44
            bytes32 value;
    45
        }
    46
    
                                                    
                                                
    47
        struct Uint256Slot {
    48
            uint256 value;
    49
        }
    50
    
                                                    
                                                
    51
        struct Int256Slot {
    52
            int256 value;
    53
        }
    54
    
                                                    
                                                
    55
        struct StringSlot {
    56
            string value;
    57
        }
    58
    
                                                    
                                                
    59
        struct BytesSlot {
    60
            bytes value;
    61
        }
    62
    
                                                    
                                                
    63
        /**
    64
         * @dev Returns an `AddressSlot` with member `value` located at `slot`.
    65
         */
    66
        function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
    67
            assembly ("memory-safe") {
    68
                r.slot := slot
    69
            }
    70
        }
    71
    
                                                    
                                                
    72
        /**
    73
         * @dev Returns a `BooleanSlot` with member `value` located at `slot`.
    74
         */
    75
        function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
    76
            assembly ("memory-safe") {
    77
                r.slot := slot
    78
            }
    79
        }
    80
    
                                                    
                                                
    81
        /**
    82
         * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.
    83
         */
    84
        function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
    85
            assembly ("memory-safe") {
    86
                r.slot := slot
    87
            }
    88
        }
    89
    
                                                    
                                                
    90
        /**
    91
         * @dev Returns a `Uint256Slot` with member `value` located at `slot`.
    92
         */
    93
        function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
    94
            assembly ("memory-safe") {
    95
                r.slot := slot
    96
            }
    97
        }
    98
    
                                                    
                                                
    99
        /**
    100
         * @dev Returns a `Int256Slot` with member `value` located at `slot`.
    101
         */
    102
        function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
    103
            assembly ("memory-safe") {
    104
                r.slot := slot
    105
            }
    106
        }
    107
    
                                                    
                                                
    108
        /**
    109
         * @dev Returns a `StringSlot` with member `value` located at `slot`.
    110
         */
    111
        function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
    112
            assembly ("memory-safe") {
    113
                r.slot := slot
    114
            }
    115
        }
    116
    
                                                    
                                                
    117
        /**
    118
         * @dev Returns an `StringSlot` representation of the string storage pointer `store`.
    119
         */
    120
        function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
    121
            assembly ("memory-safe") {
    122
                r.slot := store.slot
    123
            }
    124
        }
    125
    
                                                    
                                                
    126
        /**
    127
         * @dev Returns a `BytesSlot` with member `value` located at `slot`.
    128
         */
    129
        function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
    130
            assembly ("memory-safe") {
    131
                r.slot := slot
    132
            }
    133
        }
    134
    
                                                    
                                                
    135
        /**
    136
         * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
    137
         */
    138
        function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
    139
            assembly ("memory-safe") {
    140
                r.slot := store.slot
    141
            }
    142
        }
    143
    }
    144
    
                                                    
                                                
    0.0% lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    // OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)
    3
    
                                                    
                                                
    4
    pragma solidity >=0.4.16;
    5
    
                                                    
                                                
    6
    /**
    7
     * @dev Interface of the ERC-165 standard, as defined in the
    8
     * https://eips.ethereum.org/EIPS/eip-165[ERC].
    9
     *
    10
     * Implementers can declare support of contract interfaces, which can then be
    11
     * queried by others ({ERC165Checker}).
    12
     *
    13
     * For an implementation, see {ERC165}.
    14
     */
    15
    interface IERC165 {
    16
        /**
    17
         * @dev Returns true if this contract implements the interface defined by
    18
         * `interfaceId`. See the corresponding
    19
         * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
    20
         * to learn more about how these ids are created.
    21
         *
    22
         * This function call must use less than 30 000 gas.
    23
         */
    24
        function supportsInterface(bytes4 interfaceId) external view returns (bool);
    25
    }
    26
    
                                                    
                                                
    58.2% src/CreditPolicy.sol
    Lines covered: 57 / 98 (58.2%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    import {ICreditPolicy} from "./interfaces/ICreditPolicy.sol";
    4
    
                                                    
                                                
    5
    
                                                    
                                                
    6
    /**
    7
     * @title CreditPolicy
    8
     * @notice Immutable-by-version credit constitution for private credit funds
    9
     */
    10
    contract CreditPolicy is ICreditPolicy {
    11
        /*//////////////////////////////////////////////////////////////
    12
                                    ERRORS
    13
        //////////////////////////////////////////////////////////////*/
    14
        error CreditPolicy__Unauthorized();
    15
        error CreditPolicy__PolicyFrozen(uint256 version);
    16
        error CreditPolicy__InvalidVersion();
    17
        error CreditPolicy__PolicyVersionExists(uint256 version);
    18
        error CreditPolicy__InvalidAdmin();
    19
        error CreditPolicy__PolicyNotEditable(uint256 version);
    20
        error CreditPolicy__IncompletePolicy(uint256 version);
    21
        error CreditPolicy__InvalidIndustryHash();
    22
        error CreditPolicy__PolicyNotActive(uint256 version);
    23
        error CreditPolicy__InvalidTierCount(uint256 count);
    24
        /*//////////////////////////////////////////////////////////////
    25
                                    MODIFIERS
    26
        //////////////////////////////////////////////////////////////*/
    27
    ✓ 1
        modifier onlyAdmin() {
    28
            _onlyAdmin();
    29
            _;
    30
        }
    31
    
                                                    
                                                
    32
        function _onlyAdmin() internal view {
    33
    ✓ 10
            if (msg.sender != policyAdmin) revert CreditPolicy__Unauthorized();
    34
        }
    35
    
                                                    
                                                
    36
        modifier policyEditable(uint256 version) {
    37
    ✓ 1
            _policyEditable(version);
    38
            _;
    39
        }
    40
    
                                                    
                                                
    41
        function _policyEditable(uint256 version) internal view {
    42
    ✓ 7
            if (policyFrozen[version] || !policyActive[version])
    43
                revert CreditPolicy__PolicyNotEditable(version);
    44
        }
    45
    
                                                    
                                                
    46
        modifier policyExists(uint256 version) {
    47
    ✓ 1
            _policyExists(version);
    48
            _;
    49
        }
    50
    
                                                    
                                                
    51
        function _policyExists(uint256 version) internal view {
    52
    ✓ 8
            if (!policyCreated[version]) revert CreditPolicy__InvalidVersion();
    53
        }
    54
    
                                                    
                                                
    55
        /*//////////////////////////////////////////////////////////////
    56
                                    CORE ROLES
    57
        //////////////////////////////////////////////////////////////*/
    58
        address public policyAdmin;
    59
        uint8 internal maxTiers;
    60
    
                                                    
                                                
    61
        /*//////////////////////////////////////////////////////////////
    62
                                POLICY LIFECYCLE
    63
        //////////////////////////////////////////////////////////////*/
    64
        mapping(uint256 => bool) public policyCreated;
    65
    ✓ 165.9K
        mapping(uint256 => bool) public policyFrozen;
    66
        mapping(uint256 => bool) public policyActive;
    67
        mapping(uint256 => uint256) public lastUpdated;
    68
    
                                                    
                                                
    69
        /*//////////////////////////////////////////////////////////////
    70
                            ELIGIBILITY (PRE-LOAN)
    71
        //////////////////////////////////////////////////////////////*/
    72
        struct EligibilityCriteria {
    73
            uint256 minAnnualRevenue;
    74
            uint256 minEBITDA;
    75
            uint256 minTangibleNetWorth;
    76
            uint256 minBusinessAgeDays;
    77
            uint256 maxDefaultsLast36Months;
    78
            bool bankruptcyExcluded;
    79
        }
    80
    
                                                    
                                                
    81
        mapping(uint256 => EligibilityCriteria) public eligibility;
    82
    
                                                    
                                                
    83
        /*//////////////////////////////////////////////////////////////
    84
                            FINANCIAL RATIOS (UNDERWRITING)
    85
        //////////////////////////////////////////////////////////////*/
    86
        struct FinancialRatios {
    87
            uint256 maxTotalDebtToEBITDA;
    88
            uint256 minInterestCoverageRatio;
    89
            uint256 minCurrentRatio;
    90
            uint256 minEBITDAMarginBps;
    91
        }
    92
    
                                                    
                                                
    93
        mapping(uint256 => FinancialRatios) public ratios;
    94
    
                                                    
                                                
    95
        /*//////////////////////////////////////////////////////////////
    96
                            LOAN TIERS (PRICING REFERENCE)
    97
        //////////////////////////////////////////////////////////////*/
    98
        struct LoanTier {
    99
            string name;
    100
            uint256 minRevenue;
    101
            uint256 maxRevenue;
    102
            uint256 minEBITDA;
    103
            uint256 maxDebtToEBITDA;
    104
            uint256 maxLoanToEBITDA;
    105
            uint256 interestRateBps;
    106
            uint256 originationFeeBps;
    107
            uint256 termDays;
    108
            bool active;
    109
        }
    110
    
                                                    
                                                
    111
        mapping(uint256 => mapping(uint8 => LoanTier)) public loanTiers;
    112
        mapping(uint256 => uint8) public totalTiers;
    113
    ✓ 82.9K
        mapping(uint256 => mapping(uint8 => bool)) public tierExists;
    114
    
                                                    
                                                
    115
        /*//////////////////////////////////////////////////////////////
    116
                            CONCENTRATION LIMITS
    117
        //////////////////////////////////////////////////////////////*/
    118
        struct ConcentrationLimits {
    119
            uint256 maxSingleBorrowerBps;
    120
            uint256 maxIndustryConcentrationBps;
    121
        }
    122
    
                                                    
                                                
    123
        mapping(uint256 => ConcentrationLimits) public concentration;
    124
    
                                                    
                                                
    125
        /*//////////////////////////////////////////////////////////////
    126
                            INDUSTRY EXCLUSIONS
    127
        //////////////////////////////////////////////////////////////*/
    128
        mapping(uint256 => mapping(bytes32 => bool)) public excludedIndustries;
    129
    
                                                    
                                                
    130
        /*//////////////////////////////////////////////////////////////
    131
                            ATTESTATION REQUIREMENTS
    132
        //////////////////////////////////////////////////////////////*/
    133
        struct AttestationRequirements {
    134
            uint256 maxAttestationAgeDays;
    135
            uint256 reAttestationFrequencyDays;
    136
            bool requiresCPAAttestation;
    137
        }
    138
    
                                                    
                                                
    139
        mapping(uint256 => AttestationRequirements) public attestation;
    140
    
                                                    
                                                
    141
        /*//////////////////////////////////////////////////////////////
    142
                            MAINTENANCE COVENANTS
    143
        //////////////////////////////////////////////////////////////*/
    144
        struct MaintenanceCovenants {
    145
            uint256 maxLeverageRatio;
    146
            uint256 minCoverageRatio;
    147
            uint256 minLiquidityAmount;
    148
            bool allowsDividends;
    149
            uint256 reportingFrequencyDays;
    150
        }
    151
    
                                                    
                                                
    152
        mapping(uint256 => MaintenanceCovenants) public covenants;
    153
    
                                                    
                                                
    154
        /*//////////////////////////////////////////////////////////////
    155
                            DOCUMENT ANCHORING
    156
        //////////////////////////////////////////////////////////////*/
    157
        mapping(uint256 => bytes32) public policyDocumentHash;
    158
        mapping(uint256 => string) public policyDocumentURI;
    159
    
                                                    
                                                
    160
        mapping(uint256 => bool) public eligibilitySet;
    161
        mapping(uint256 => bool) public ratiosSet;
    162
        mapping(uint256 => bool) public concentrationSet;
    163
        mapping(uint256 => bool) public attestationSet;
    164
        mapping(uint256 => bool) public covenantsSet;
    165
        mapping(uint256 => bool) public hasAtLeastOneTier;
    166
    
                                                    
                                                
    167
        /*//////////////////////////////////////////////////////////////
    168
                                    EVENTS
    169
        //////////////////////////////////////////////////////////////*/
    170
        event PolicyCreated(uint256 version, uint256 timestamp);
    171
        event PolicyFrozen(uint256 version, uint256 timestamp);
    172
        event PolicyEligibilityUpdated(uint256 version, uint256 timestamp);
    173
        event PolicyRatiosUpdated(uint256 version, uint256 timestamp);
    174
        event PolicyConcentrationUpdated(uint256 version, uint256 timestamp);
    175
        event PolicyAttestationUpdated(uint256 version, uint256 timestamp);
    176
        event PolicyCovenantsUpdated(uint256 version, uint256 timestamp);
    177
        event LoanTierUpdated(uint256 version, uint8 tierId, uint256 timestamp);
    178
        event IndustryExcluded(
    179
            uint256 version,
    180
            bytes32 industry,
    181
            uint256 timestamp
    182
        );
    183
        event MaxTiersChanged(uint8 maxTiers);
    184
        event IndustryIncluded(
    185
            uint256 version,
    186
            bytes32 industry,
    187
            uint256 timestamp
    188
        );
    189
    
                                                    
                                                
    190
        event PolicyAdminChanged(address newAdmin);
    191
    
                                                    
                                                
    192
        event PolicyDocumentSet(
    193
            uint256 version,
    194
            bytes32 hash,
    195
            string uri,
    196
            uint256 timestamp
    197
        );
    198
        event PolicyDeactivated(uint256 version, uint256 timestamp);
    199
    
                                                    
                                                
    200
        /*//////////////////////////////////////////////////////////////
    201
                                    CONSTRUCTOR
    202
        //////////////////////////////////////////////////////////////*/
    203
        constructor() {
    204
    ✓ 1
            policyAdmin = msg.sender;
    205
        }
    206
    
                                                    
                                                
    207
        /*//////////////////////////////////////////////////////////////
    208
                            POLICY CREATION
    209
        //////////////////////////////////////////////////////////////*/
    210
    
                                                    
                                                
    211
        function createPolicy(uint256 version) external onlyAdmin {
    212
    ✓ 1
            if (version == 0) {
    213
                revert CreditPolicy__InvalidVersion();
    214
            }
    215
            if (policyCreated[version]) {
    216
                revert CreditPolicy__PolicyVersionExists(version);
    217
            }
    218
    
                                                    
                                                
    219
            policyCreated[version] = true;
    220
    ✓ 1
            policyActive[version] = true;
    221
    ✓ 1
            lastUpdated[version] = block.timestamp;
    222
    ✓ 1
            emit PolicyCreated(version, block.timestamp);
    223
        }
    224
    
                                                    
                                                
    225
        function freezePolicy(
    226
            uint256 version
    227
        ) external onlyAdmin policyExists(version) {
    228
    ✓ 1
            if (policyFrozen[version]) {
    229
                revert CreditPolicy__PolicyFrozen(version);
    230
            }
    231
    ✓ 1
            if (policyActive[version] == false) {
    232
                revert CreditPolicy__PolicyNotActive(version);
    233
            }
    234
            if (
    235
    ✓ 1
                !eligibilitySet[version] ||
    236
    ✓ 1
                !ratiosSet[version] ||
    237
    ✓ 1
                !concentrationSet[version] ||
    238
    ✓ 1
                !attestationSet[version] ||
    239
    ✓ 1
                !covenantsSet[version] ||
    240
    ✓ 1
                !hasAtLeastOneTier[version]
    241
            ) {
    242
                revert CreditPolicy__IncompletePolicy(version);
    243
            }
    244
    ✓ 1
            if (policyDocumentHash[version] == bytes32(0)) {
    245
                revert CreditPolicy__IncompletePolicy(version);
    246
            }
    247
    
                                                    
                                                
    248
            policyFrozen[version] = true;
    249
    ✓ 1
            lastUpdated[version] = block.timestamp;
    250
    ✓ 1
            emit PolicyFrozen(version, block.timestamp);
    251
        }
    252
    
                                                    
                                                
    253
        function deActivatePolicy(
    254
            uint256 version
    255
        ) external onlyAdmin policyExists(version) {
    256
            policyActive[version] = false;
    257
            lastUpdated[version] = block.timestamp;
    258
            emit PolicyDeactivated(version, block.timestamp);
    259
        }
    260
    
                                                    
                                                
    261
        /*//////////////////////////////////////////////////////////////
    262
                            ELIGIBILITY UPDATE
    263
        //////////////////////////////////////////////////////////////*/
    264
        function updateEligibility(
    265
            uint256 version,
    266
            EligibilityCriteria calldata data
    267
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    268
    ✓ 1
            eligibility[version] = data;
    269
    ✓ 1
            lastUpdated[version] = block.timestamp;
    270
    ✓ 1
            eligibilitySet[version] = true;
    271
    ✓ 1
            emit PolicyEligibilityUpdated(version, block.timestamp);
    272
        }
    273
    
                                                    
                                                
    274
        /*//////////////////////////////////////////////////////////////
    275
                            RATIOS UPDATE
    276
        //////////////////////////////////////////////////////////////*/
    277
        function updateRatios(
    278
            uint256 version,
    279
            FinancialRatios calldata data
    280
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    281
    ✓ 1
            ratios[version] = data;
    282
    ✓ 1
            lastUpdated[version] = block.timestamp;
    283
    ✓ 1
            ratiosSet[version] = true;
    284
    ✓ 1
            emit PolicyRatiosUpdated(version, block.timestamp);
    285
        }
    286
    
                                                    
                                                
    287
        /*//////////////////////////////////////////////////////////////
    288
                            CONCENTRATION UPDATE
    289
        //////////////////////////////////////////////////////////////*/
    290
        function updateConcentration(
    291
            uint256 version,
    292
            ConcentrationLimits calldata data
    293
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    294
    ✓ 1
            concentration[version] = data;
    295
    ✓ 1
            lastUpdated[version] = block.timestamp;
    296
    ✓ 1
            concentrationSet[version] = true;
    297
    ✓ 1
            emit PolicyConcentrationUpdated(version, block.timestamp);
    298
        }
    299
    
                                                    
                                                
    300
        /*//////////////////////////////////////////////////////////////
    301
                            ATTESTATION UPDATE
    302
        //////////////////////////////////////////////////////////////*/
    303
        function updateAttestation(
    304
            uint256 version,
    305
            AttestationRequirements calldata data
    306
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    307
    ✓ 1
            attestation[version] = data;
    308
    ✓ 1
            lastUpdated[version] = block.timestamp;
    309
    ✓ 1
            attestationSet[version] = true;
    310
    ✓ 1
            emit PolicyAttestationUpdated(version, block.timestamp);
    311
        }
    312
    
                                                    
                                                
    313
        /*//////////////////////////////////////////////////////////////
    314
                            COVENANT UPDATE
    315
        //////////////////////////////////////////////////////////////*/
    316
        function updateCovenants(
    317
            uint256 version,
    318
            MaintenanceCovenants calldata data
    319
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    320
    ✓ 1
            covenants[version] = data;
    321
    ✓ 1
            lastUpdated[version] = block.timestamp;
    322
    ✓ 1
            covenantsSet[version] = true;
    323
    ✓ 1
            emit PolicyCovenantsUpdated(version, block.timestamp);
    324
        }
    325
    
                                                    
                                                
    326
        /*//////////////////////////////////////////////////////////////
    327
                            LOAN TIERS
    328
        //////////////////////////////////////////////////////////////*/
    329
        function setLoanTier(
    330
            uint256 version,
    331
            uint8 tierId,
    332
            LoanTier calldata tier
    333
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    334
    ✓ 1
            if (tierId >= maxTiers) {
    335
                revert CreditPolicy__InvalidTierCount(tierId);
    336
            }
    337
    ✓ 1
            loanTiers[version][tierId] = tier;
    338
            tierExists[version][tierId] = true;
    339
    ✓ 1
            if (tierId >= totalTiers[version]) {
    340
                totalTiers[version] = tierId + 1;
    341
            }
    342
    ✓ 1
            hasAtLeastOneTier[version] = true;
    343
    ✓ 1
            lastUpdated[version] = block.timestamp;
    344
    ✓ 1
            emit LoanTierUpdated(version, tierId, block.timestamp);
    345
        }
    346
    
                                                    
                                                
    347
        /*//////////////////////////////////////////////////////////////
    348
                            INDUSTRY CONTROLS
    349
        //////////////////////////////////////////////////////////////*/
    350
        function excludeIndustry(
    351
            uint256 version,
    352
            bytes32 industry
    353
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    354
            if (industry == bytes32(0)) {
    355
                revert CreditPolicy__InvalidIndustryHash();
    356
            }
    357
            excludedIndustries[version][industry] = true;
    358
            lastUpdated[version] = block.timestamp;
    359
            emit IndustryExcluded(version, industry, block.timestamp);
    360
        }
    361
    
                                                    
                                                
    362
        function includeIndustry(
    363
            uint256 version,
    364
            bytes32 industry
    365
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    366
            if (industry == bytes32(0)) {
    367
                revert CreditPolicy__InvalidIndustryHash();
    368
            }
    369
            excludedIndustries[version][industry] = false;
    370
            lastUpdated[version] = block.timestamp;
    371
            emit IndustryIncluded(version, industry, block.timestamp);
    372
        }
    373
    
                                                    
                                                
    374
        /*//////////////////////////////////////////////////////////////
    375
                            DOCUMENT UPDATE
    376
        //////////////////////////////////////////////////////////////*/
    377
        function setPolicyDocument(
    378
            uint256 version,
    379
            bytes32 hash,
    380
            string calldata uri
    381
        ) external onlyAdmin policyExists(version) policyEditable(version) {
    382
    ✓ 1
            policyDocumentHash[version] = hash;
    383
    ✓ 1
            policyDocumentURI[version] = uri;
    384
    ✓ 1
            lastUpdated[version] = block.timestamp;
    385
    
                                                    
                                                
    386
    ✓ 1
            emit PolicyDocumentSet(version, hash, uri, block.timestamp);
    387
        }
    388
    
                                                    
                                                
    389
        function changePolicyAdmin(address newAdmin) external onlyAdmin {
    390
            if (newAdmin == address(0)) {
    391
                revert CreditPolicy__InvalidAdmin();
    392
            }
    393
            policyAdmin = newAdmin;
    394
    
                                                    
                                                
    395
            emit PolicyAdminChanged(newAdmin);
    396
        }
    397
    
                                                    
                                                
    398
        // getters for interface compliance
    399
    
                                                    
                                                
    400
        function isPolicyActive(uint256 version) external view returns (bool) {
    401
            return policyActive[version];
    402
        }
    403
    
                                                    
                                                
    404
        function isPolicyFrozen(uint256 version) external view returns (bool) {
    405
            return policyFrozen[version];
    406
        }
    407
    
                                                    
                                                
    408
        function tierExistsInPolicy(
    409
            uint256 version,
    410
            uint8 tierId
    411
        ) external view returns (bool) {
    412
            return tierExists[version][tierId];
    413
        }
    414
    
                                                    
                                                
    415
        function setMaxTiers(uint8 _maxTiers) external onlyAdmin {
    416
    ✓ 1
            if (_maxTiers == 255) {
    417
                revert CreditPolicy__InvalidTierCount(_maxTiers);
    418
            }
    419
            maxTiers = _maxTiers;
    420
    ✓ 1
            emit MaxTiersChanged(_maxTiers);
    421
        }
    422
    
                                                    
                                                
    423
        function getMaxTiers() external view returns (uint8) {
    424
            return maxTiers;
    425
        }
    426
    
                                                    
                                                
    427
        function isIndustryExcluded(
    428
            uint256 version,
    429
            bytes32 industry
    430
        ) external view returns (bool) {
    431
    ✓ 82.9K
            return excludedIndustries[version][industry];
    432
        }
    433
    }
    434
    
                                                    
                                                
    66.1% src/LoanEngine.sol
    Lines covered: 84 / 127 (66.1%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    
                                                    
                                                
    4
    import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
    5
    import {ICreditPolicy} from "./interfaces/ICreditPolicy.sol";
    6
    import {IVerifier} from "./interfaces/IVerifier.sol";
    7
    import {ITranchePool} from "./interfaces/ITranchePool.sol";
    8
    import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
    9
    import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    10
    import {TranchePool} from "./TranchePool.sol";
    11
    
                                                    
                                                
    12
    contract LoanEngine is Ownable, ReentrancyGuard {
    13
        using SafeERC20 for IERC20;
    14
        /*//////////////////////////////////////////////////////////////
    15
                            ERRORS
    16
        //////////////////////////////////////////////////////////////*/
    17
    
                                                    
                                                
    18
        error LoanEngine__PolicyNotFrozen(uint256 policyVersion);
    19
        error LoanEngine__InvalidProof();
    20
        error LoanEngine__LoanTierIsNotInPolicy(
    21
            uint256 policyVersion,
    22
            uint8 tierId
    23
        );
    24
        error LoanEngine__InvalidLoanParameters(
    25
            uint256 loanId,
    26
            uint256 principalIssued,
    27
            uint256 aprBps,
    28
            uint256 termDays
    29
        );
    30
        error LoanEngine__MaxOriginationFeeExceeded(
    31
            uint256 loanId,
    32
            uint256 originationFeeBps,
    33
            uint256 maxOriginationFeeBps
    34
        );
    35
        error LoanEngine__PoolNotDeployed();
    36
        error LoanEngine__InvalidOffRampingEntity(address entity);
    37
        error LoanEngine__LoanExists(uint256 loanId);
    38
        error LoanEngine__LoanIsNotInCreatedState(uint256 loanId);
    39
        error LoanEngine__LoanIsNotActive(uint256 loanId);
    40
        error LoanEngine__LoanIsNotDefaulted(uint256 loanId);
    41
        error LoanEngine__InvalidRepayment();
    42
        error LoanEngine__ZeroRecovery();
    43
        error LoanEngine__LoanNotRecoverable(uint256 loanId);
    44
        error LoanEngine__ZeroLossOnWriteOff(uint256 loanId);
    45
        error LoanEngine__InvalidFeeManagerEntity(address manager);
    46
        error LoanEngine__InvalidRecoveryAgent(address agent);
    47
        error LoanEngine__InvalidRepaymentAgent(address agent);
    48
        error LoanEngine__InsufficientPoolLiquidity();
    49
        error LoanEngine__ProofAlreadyUsed();
    50
        modifier isWhiteListedOffRampingEntity(address entity) {
    51
            _isWhiteListedOffRampingEntity(entity);
    52
            _;
    53
        }
    54
    
                                                    
                                                
    55
        function _isWhiteListedOffRampingEntity(address entity) internal view {
    56
    ✓ 35.3K
            if (!whitelistedOffRampingEntities[entity]) {
    57
                revert LoanEngine__InvalidOffRampingEntity(entity);
    58
            }
    59
        }
    60
    
                                                    
                                                
    61
        modifier isWhiteListedRecoveryAgent(address agent) {
    62
            _isWhiteListedRecoveryAgent(agent);
    63
            _;
    64
        }
    65
    
                                                    
                                                
    66
        function _isWhiteListedRecoveryAgent(address agent) internal view {
    67
            if (!whitelistedRecoveryAgents[agent]) {
    68
                revert LoanEngine__InvalidRecoveryAgent(agent);
    69
            }
    70
        }
    71
    
                                                    
                                                
    72
        modifier isWhiteListedRepaymentAgent(address agent) {
    73
            _isWhiteListedRepaymentAgent(agent);
    74
            _;
    75
        }
    76
    
                                                    
                                                
    77
        function _isWhiteListedRepaymentAgent(address agent) internal view {
    78
    ✓ 17.2K
            if (!whitelistedRepaymentAgents[agent]) {
    79
                revert LoanEngine__InvalidRepaymentAgent(agent);
    80
            }
    81
        }
    82
    
                                                    
                                                
    83
        modifier isWhiteListedFeeManager(address manager) {
    84
            _isWhiteListedFeeManager(manager);
    85
            _;
    86
        }
    87
    
                                                    
                                                
    88
        function _isWhiteListedFeeManager(address manager) internal view {
    89
    ✓ 35.3K
            if (!whitelistedFeeManagers[manager]) {
    90
                revert LoanEngine__InvalidFeeManagerEntity(manager);
    91
            }
    92
        }
    93
    
                                                    
                                                
    94
        ICreditPolicy public creditPolicyContract;
    95
        IVerifier loanProofVerifier;
    96
        ITranchePool tranchePool;
    97
    
                                                    
                                                
    98
        mapping(uint256 loanId => Loan) public s_loans;
    99
        mapping(uint256 loanId => uint256) public s_originationFees;
    100
        mapping(address whitelistedOffRampingEntity => bool)
    101
            public whitelistedOffRampingEntities;
    102
        mapping(address whiteListedRecoveryAgent => bool)
    103
            public whitelistedRecoveryAgents;
    104
    
                                                    
                                                
    105
        mapping(address whiteListedRepaymentAgent => bool)
    106
            public whitelistedRepaymentAgents;
    107
    
                                                    
                                                
    108
        mapping(address whiteListedFeeManager => bool)
    109
            public whitelistedFeeManagers;
    110
        mapping(bytes32 nullifierHash => bool) public s_nullifierHashes;
    111
    ✓ 1
        uint256 public s_nextLoanId = 1;
    112
    ✓ 82.9K
        uint256 public s_maxOriginationFeeBps;
    113
        address public s_stableCoinAddress;
    114
        uint256 public constant STANDARD_BPS = 100;
    115
        enum LoanState {
    116
            NONE,
    117
            CREATED,
    118
            ACTIVE,
    119
            REPAID,
    120
            DEFAULTED,
    121
            WRITTEN_OFF
    122
        }
    123
    
                                                    
                                                
    124
        struct Loan {
    125
            // Identity
    126
            uint256 loanId;
    127
            bytes32 borrowerCommitment;
    128
            uint256 policyVersion;
    129
            uint8 tierId;
    130
            // Economics
    131
            uint256 principalIssued;
    132
            uint256 principalOutstanding;
    133
            uint256 aprBps;
    134
            uint256 originationFeeBps;
    135
            // Interest accounting
    136
            uint256 interestAccrued;
    137
            uint256 interestPaid;
    138
            uint256 lastAccrualTimestamp;
    139
            // Timing
    140
            uint256 startTimestamp;
    141
            uint256 maturityTimestamp;
    142
            uint256 termDays;
    143
            // State
    144
            LoanState state;
    145
            uint256 totalRecovered;
    146
            // allocation_ratio
    147
            uint256 seniorPrincipalAllocated;
    148
            uint256 juniorPrincipalAllocated;
    149
        }
    150
    
                                                    
                                                
    151
        /*//////////////////////////////////////////////////////////////
    152
                            EVENTS
    153
        //////////////////////////////////////////////////////////////*/
    154
    
                                                    
                                                
    155
        event LoanCreated(
    156
            uint256 indexed loanId,
    157
            bytes32 borrowerCommitment,
    158
            uint256 principalIssued,
    159
            uint8 tierId,
    160
            uint256 timestamp
    161
        );
    162
    
                                                    
                                                
    163
        event LoanActivated(
    164
            uint256 indexed loanId,
    165
            uint256 principalIssued,
    166
            uint256 timestamp,
    167
            uint256 startTimestamp,
    168
            uint256 maturityTimestamp
    169
        );
    170
    
                                                    
                                                
    171
        event LoanRepaid(
    172
            uint256 indexed loanId,
    173
            uint256 principalRepaid,
    174
            uint256 interestRepaid,
    175
            uint256 timestamp
    176
        );
    177
    
                                                    
                                                
    178
        event LoanClosed(uint256 indexed loanId, uint256 timestamp);
    179
    
                                                    
                                                
    180
        event LoanDefaulted(
    181
            uint256 indexed loanId,
    182
            bytes32 reasonHash,
    183
            uint256 timestamp
    184
        );
    185
    
                                                    
                                                
    186
        event LoanWrittenOff(uint256 indexed loanId, uint256 timestamp);
    187
    
                                                    
                                                
    188
        event LoanRecovered(
    189
            uint256 indexed loanId,
    190
            uint256 amount,
    191
            uint256 timestamp
    192
        );
    193
    
                                                    
                                                
    194
        constructor(
    195
            address _creditPolicyContract,
    196
            address _loanProofVerifier,
    197
            uint256 _maxOriginationFeeBps,
    198
            address _tranchePool,
    199
            address _stableCoinAddress
    200
    ✓ 1
        ) Ownable(msg.sender) {
    201
            creditPolicyContract = ICreditPolicy(_creditPolicyContract);
    202
    ✓ 1
            loanProofVerifier = IVerifier(_loanProofVerifier);
    203
    ✓ 1
            s_maxOriginationFeeBps = _maxOriginationFeeBps;
    204
    ✓ 1
            tranchePool = ITranchePool(_tranchePool);
    205
    ✓ 1
            s_stableCoinAddress = _stableCoinAddress;
    206
        }
    207
    
                                                    
                                                
    208
        // A notarization step that records a policy-compliant loan intent on-chain
    209
        // TODO: public inputs needed to be verified against the contract state
    210
        // will be implemented after the public inputs structure is finalized
    211
        // preconditions
    212
        // borrowerCommitment should match the publicinput commitment
    213
        // all the parameteres should match the public inputs
    214
        function createLoan(
    215
            bytes32 borrowerCommitment,
    216
            bytes32 nullifierHash,
    217
            uint256 policyVersion,
    218
            uint8 tierId,
    219
            uint256 principalIssued,
    220
            uint256 aprBps,
    221
            uint256 originationFeeBps,
    222
            uint256 termDays,
    223
            bytes32 industry,
    224
            bytes calldata proofData,
    225
            bytes32[] calldata publicInputs
    226
        ) external onlyOwner {
    227
    ✓ 82.9K
            if (s_loans[s_nextLoanId].state != LoanState.NONE) {
    228
                revert LoanEngine__LoanExists(s_nextLoanId);
    229
            }
    230
            // Implementation goes here
    231
    ✓ 82.9K
            if (!creditPolicyContract.isPolicyFrozen(policyVersion)) {
    232
                revert LoanEngine__PolicyNotFrozen(policyVersion);
    233
            }
    234
    
                                                    
                                                
    235
    ✓ 82.9K
            if (creditPolicyContract.isIndustryExcluded(policyVersion, industry)) {
    236
                revert LoanEngine__PolicyNotFrozen(policyVersion);
    237
            }
    238
    
                                                    
                                                
    239
    ✓ 82.9K
            if (!creditPolicyContract.tierExistsInPolicy(policyVersion, tierId)) {
    240
                revert LoanEngine__LoanTierIsNotInPolicy(policyVersion, tierId);
    241
            }
    242
    
                                                    
                                                
    243
    ✓ 82.9K
            if (s_nullifierHashes[nullifierHash]) {
    244
                revert LoanEngine__ProofAlreadyUsed();
    245
            }
    246
    
                                                    
                                                
    247
    ✓ 82.9K
            if (loanProofVerifier.verify(proofData, publicInputs) == false) {
    248
                revert LoanEngine__InvalidProof();
    249
            }
    250
    
                                                    
                                                
    251
            if (
    252
    ✓ 82.9K
                tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED &&
    253
    ✓ 125.3K
                tranchePool.getPoolState() != TranchePool.PoolState.COMMITED
    254
            ) {
    255
                revert LoanEngine__PoolNotDeployed();
    256
            }
    257
    
                                                    
                                                
    258
    ✓ 82.9K
            if (principalIssued == 0 || aprBps == 0 || termDays == 0) {
    259
                revert LoanEngine__InvalidLoanParameters(
    260
                    s_nextLoanId,
    261
                    principalIssued,
    262
                    aprBps,
    263
                    termDays
    264
                );
    265
            }
    266
    
                                                    
                                                
    267
    ✓ 82.9K
            if (originationFeeBps > s_maxOriginationFeeBps) {
    268
                revert LoanEngine__MaxOriginationFeeExceeded(
    269
                    s_nextLoanId,
    270
                    originationFeeBps,
    271
                    s_maxOriginationFeeBps
    272
                );
    273
            }
    274
    
                                                    
                                                
    275
    ✓ 82.9K
            if (principalIssued > tranchePool.getTotalIdleValue()) {
    276
                revert LoanEngine__InsufficientPoolLiquidity();
    277
            }
    278
    
                                                    
                                                
    279
    ✓ 82.9K
            Loan memory newLoan = Loan({
    280
                loanId: s_nextLoanId,
    281
                borrowerCommitment: borrowerCommitment,
    282
                policyVersion: policyVersion,
    283
                tierId: tierId,
    284
                principalIssued: principalIssued,
    285
                principalOutstanding: 0,
    286
                aprBps: aprBps,
    287
                originationFeeBps: originationFeeBps,
    288
                interestAccrued: 0,
    289
                interestPaid: 0,
    290
                lastAccrualTimestamp: 0,
    291
                startTimestamp: 0,
    292
                maturityTimestamp: 0,
    293
                termDays: termDays,
    294
                state: LoanState.CREATED,
    295
                totalRecovered: 0,
    296
                seniorPrincipalAllocated: 0,
    297
                juniorPrincipalAllocated: 0
    298
            });
    299
    
                                                    
                                                
    300
            s_loans[s_nextLoanId++] = newLoan;
    301
            s_nullifierHashes[nullifierHash] = true;
    302
    
                                                    
                                                
    303
            emit LoanCreated(
    304
                newLoan.loanId,
    305
                borrowerCommitment,
    306
                principalIssued,
    307
                tierId,
    308
    ✓ 82.9K
                block.timestamp
    309
            );
    310
        }
    311
    
                                                    
                                                
    312
        /*
    313
            preconditions
    314
            - onlyOwner
    315
            - loan must already exist
    316
            - loan.state == CREATED
    317
        */
    318
        function activateLoan(
    319
            uint256 loanId,
    320
            address receivingEntity,
    321
            address feeManager
    322
        )
    323
            external
    324
            onlyOwner
    325
            isWhiteListedOffRampingEntity(receivingEntity)
    326
            isWhiteListedFeeManager(feeManager)
    327
            nonReentrant
    328
        {
    329
            // Implementation goes here
    330
            Loan storage loan = s_loans[loanId];
    331
    
                                                    
                                                
    332
    ✓ 35.3K
            if (loan.state != LoanState.CREATED) {
    333
                revert LoanEngine__LoanIsNotInCreatedState(loanId);
    334
            }
    335
    ✓ 35.3K
            loan.principalOutstanding = loan.principalIssued;
    336
    ✓ 35.3K
            loan.lastAccrualTimestamp = block.timestamp;
    337
    ✓ 35.3K
            loan.startTimestamp = block.timestamp;
    338
    ✓ 35.3K
            loan.maturityTimestamp = block.timestamp + (loan.termDays * 1 days);
    339
    ✓ 35.3K
            loan.state = LoanState.ACTIVE;
    340
    
                                                    
                                                
    341
            uint256 originationFee = (loan.principalIssued *
    342
    ✓ 35.3K
                loan.originationFeeBps) / 10000;
    343
    
                                                    
                                                
    344
            s_originationFees[loanId] = originationFee;
    345
    
                                                    
                                                
    346
    ✓ 35.3K
            if (loan.principalIssued > tranchePool.getTotalIdleValue()) {
    347
                revert LoanEngine__InsufficientPoolLiquidity();
    348
            }
    349
    
                                                    
                                                
    350
    ✓ 35.3K
            uint256 totalDisbursement = loan.principalIssued - originationFee;
    351
    ✓ 35.3K
            (uint256 seniorAmount, uint256 juniorAmount, ) = tranchePool
    352
                .allocateCapital(
    353
                    totalDisbursement,
    354
                    originationFee,
    355
                    receivingEntity,
    356
                    feeManager
    357
                );
    358
    ✓ 35.3K
            loan.seniorPrincipalAllocated = seniorAmount;
    359
    
                                                    
                                                
    360
    ✓ 35.3K
            loan.juniorPrincipalAllocated = juniorAmount;
    361
    
                                                    
                                                
    362
    ✓ 35.3K
            emit LoanActivated(
    363
                loan.loanId,
    364
                loan.principalIssued,
    365
                block.timestamp,
    366
                loan.startTimestamp,
    367
                loan.maturityTimestamp
    368
            );
    369
        }
    370
    
                                                    
                                                
    371
        function repayLoan(
    372
            uint256 loanId,
    373
            uint256 principalAmount,
    374
            uint256 interestAmount,
    375
            address repaymentAgent
    376
        )
    377
            external
    378
            onlyOwner
    379
            isWhiteListedRepaymentAgent(repaymentAgent)
    380
            nonReentrant
    381
        {
    382
            Loan storage loan = s_loans[loanId];
    383
    ✓ 17.2K
            if (loan.state != LoanState.ACTIVE) {
    384
                revert LoanEngine__LoanIsNotActive(loanId);
    385
            }
    386
    
                                                    
                                                
    387
    ✓ 17.2K
            uint256 totalPayment = principalAmount + interestAmount;
    388
    ✓ 17.2K
            if (totalPayment == 0) {
    389
                revert LoanEngine__InvalidRepayment();
    390
            }
    391
    
                                                    
                                                
    392
    ✓ 17.2K
            _accrueInterest(loanId);
    393
    
                                                    
                                                
    394
            // 1️⃣ Transfer funds to pool (settlement layer)
    395
    ✓ 17.2K
            IERC20(s_stableCoinAddress).safeTransferFrom(
    396
                repaymentAgent,
    397
    ✓ 17.2K
                address(tranchePool),
    398
    ✓ 17.2K
                totalPayment
    399
            );
    400
    
                                                    
                                                
    401
            // 2️⃣ Interest first
    402
    ✓ 17.2K
            uint256 interestDue = loan.interestAccrued;
    403
    ✓ 17.2K
            uint256 interestPaid = totalPayment > interestDue
    404
                ? interestDue
    405
                : totalPayment;
    406
    
                                                    
                                                
    407
            // 3️⃣ Principal second
    408
    ✓ 17.2K
            uint256 remainingForPrincipal = totalPayment - interestPaid;
    409
    ✓ 17.2K
            uint256 principalDue = loan.principalOutstanding;
    410
    
                                                    
                                                
    411
    ✓ 17.2K
            uint256 principalPaid = remainingForPrincipal > principalDue
    412
                ? principalDue
    413
                : remainingForPrincipal;
    414
    
                                                    
                                                
    415
            // 4️⃣ Update loan accounting
    416
    ✓ 17.2K
            loan.interestAccrued -= interestPaid;
    417
    ✓ 17.2K
            loan.interestPaid += interestPaid;
    418
    ✓ 17.2K
            loan.principalOutstanding -= principalPaid;
    419
    
                                                    
                                                
    420
    ✓ 17.2K
            bool fullyRepaid = loan.principalOutstanding == 0 &&
    421
    ✓ 9.5K
                loan.interestAccrued == 0;
    422
    
                                                    
                                                
    423
    ✓ 17.2K
            if (fullyRepaid) {
    424
                loan.state = LoanState.REPAID;
    425
            }
    426
    
                                                    
                                                
    427
    ✓ 17.2K
            tranchePool.onRepayment(principalPaid, interestPaid);
    428
    
                                                    
                                                
    429
            emit LoanRepaid(
    430
                loan.loanId,
    431
                principalPaid,
    432
                interestPaid,
    433
    ✓ 17.2K
                block.timestamp
    434
            );
    435
    
                                                    
                                                
    436
            if (fullyRepaid) {
    437
    ✓ 9.5K
                emit LoanClosed(loanId, block.timestamp);
    438
            }
    439
        }
    440
    
                                                    
                                                
    441
        function declareDefault(
    442
            uint256 loanId,
    443
            bytes32 reasonHash
    444
        ) external onlyOwner {
    445
            // Implementation goes here
    446
            Loan storage loan = s_loans[loanId];
    447
    ✓ 18
            if (loan.state != LoanState.ACTIVE) {
    448
                revert LoanEngine__LoanIsNotActive(loanId);
    449
            }
    450
    ✓ 18
            _accrueInterest(loanId);
    451
            loan.state = LoanState.DEFAULTED;
    452
    ✓ 18
            emit LoanDefaulted(loanId, reasonHash, block.timestamp);
    453
        }
    454
    
                                                    
                                                
    455
        function writeOffLoan(uint256 loanId) external onlyOwner {
    456
            // Implementation goes here
    457
            Loan storage loan = s_loans[loanId];
    458
    ✓ 5
            if (loan.state != LoanState.DEFAULTED) {
    459
                revert LoanEngine__LoanIsNotDefaulted(loanId);
    460
            }
    461
    ✓ 5
            uint256 loss = loan.principalOutstanding;
    462
    ✓ 5
            uint256 interestAccrued = loan.interestAccrued;
    463
    ✓ 5
            if (loss == 0) {
    464
                revert LoanEngine__ZeroLossOnWriteOff(loanId);
    465
            }
    466
    
                                                    
                                                
    467
            loan.principalOutstanding = 0;
    468
            loan.interestAccrued = 0;
    469
            loan.state = LoanState.WRITTEN_OFF;
    470
    ✓ 5
            tranchePool.onLoss(loss, interestAccrued);
    471
    ✓ 5
            emit LoanWrittenOff(loanId, block.timestamp);
    472
        }
    473
    
                                                    
                                                
    474
        function recoverLoan(
    475
            uint256 loanId,
    476
            uint256 amount,
    477
            address recoveryAgent
    478
        ) external onlyOwner isWhiteListedRecoveryAgent(recoveryAgent) {
    479
            Loan storage loan = s_loans[loanId];
    480
            if (loan.state != LoanState.WRITTEN_OFF) {
    481
                revert LoanEngine__LoanNotRecoverable(loanId);
    482
            }
    483
            if (amount == 0) {
    484
                revert LoanEngine__ZeroRecovery();
    485
            }
    486
            loan.totalRecovered += amount;
    487
            IERC20(s_stableCoinAddress).safeTransferFrom(
    488
                recoveryAgent,
    489
                address(tranchePool),
    490
                amount
    491
            );
    492
            tranchePool.onRecovery(amount);
    493
            emit LoanRecovered(loanId, amount, block.timestamp);
    494
        }
    495
    
                                                    
                                                
    496
        function _accrueInterest(uint256 loanId) internal {
    497
            // Implementation goes here
    498
    ✓ 17.3K
            Loan storage loan = s_loans[loanId];
    499
    ✓ 17.3K
            if (loan.state != LoanState.ACTIVE) {
    500
                revert LoanEngine__LoanIsNotActive(loanId);
    501
            }
    502
    ✓ 17.3K
            uint256 timeElapsed = block.timestamp - loan.lastAccrualTimestamp;
    503
    ✓ 17.3K
            if (loan.principalOutstanding == 0) {
    504
                loan.lastAccrualTimestamp = block.timestamp;
    505
                return;
    506
            }
    507
    
                                                    
                                                
    508
            uint256 interest = (loan.principalOutstanding *
    509
    ✓ 17.3K
                loan.aprBps *
    510
    ✓ 17.3K
                timeElapsed) / (365 days * 10_000);
    511
    
                                                    
                                                
    512
    ✓ 17.3K
            if (interest > 0) {
    513
    ✓ 17.1K
                loan.interestAccrued += interest;
    514
    ✓ 17.1K
                uint256 totalAllocated = loan.principalIssued;
    515
                uint256 seniorInterest = (interest *
    516
    ✓ 17.1K
                    loan.seniorPrincipalAllocated) / totalAllocated;
    517
    
                                                    
                                                
    518
                uint256 juniorInterest = (interest *
    519
    ✓ 17.1K
                    loan.juniorPrincipalAllocated) / totalAllocated;
    520
    ✓ 17.1K
                tranchePool.onInterestAccrued(
    521
                    interest,
    522
                    seniorInterest,
    523
                    juniorInterest
    524
                );
    525
            }
    526
            loan.lastAccrualTimestamp = block.timestamp;
    527
        }
    528
    
                                                    
                                                
    529
        // setters for contract management
    530
    
                                                    
                                                
    531
        function setMaxOriginationFeeBps(
    532
            uint256 _maxOriginationFeeBps
    533
        ) external onlyOwner {
    534
    ✓ 1
            s_maxOriginationFeeBps = _maxOriginationFeeBps;
    535
        }
    536
    
                                                    
                                                
    537
        function setWhitelistedOffRampingEntity(
    538
            address entity,
    539
            bool isWhitelisted
    540
        ) external onlyOwner {
    541
    ✓ 1
            whitelistedOffRampingEntities[entity] = isWhitelisted;
    542
        }
    543
    
                                                    
                                                
    544
        function setWhitelistedRecoveryAgent(
    545
            address agent,
    546
            bool isWhitelisted
    547
        ) external onlyOwner {
    548
    ✓ 1
            whitelistedRecoveryAgents[agent] = isWhitelisted;
    549
        }
    550
    
                                                    
                                                
    551
        function setWhitelistedRepaymentAgent(
    552
            address agent,
    553
            bool isWhitelisted
    554
        ) external onlyOwner {
    555
    ✓ 1
            whitelistedRepaymentAgents[agent] = isWhitelisted;
    556
        }
    557
    
                                                    
                                                
    558
        function setWhitelistedFeeManager(
    559
            address manager,
    560
            bool isWhitelisted
    561
        ) external onlyOwner {
    562
    ✓ 1
            whitelistedFeeManagers[manager] = isWhitelisted;
    563
        }
    564
    
                                                    
                                                
    565
        function getMaxOriginationFeeBps() external view returns (uint256) {
    566
            return s_maxOriginationFeeBps;
    567
        }
    568
    
                                                    
                                                
    569
        function getNextLoanId() external view returns (uint256) {
    570
    ✓ 861.8K
            return s_nextLoanId;
    571
        }
    572
    
                                                    
                                                
    573
        function getLoanDetails(
    574
            uint256 loanId
    575
        ) external view returns (Loan memory) {
    576
            return s_loans[loanId];
    577
        }
    578
    }
    579
    
                                                    
                                                
    51.9% src/TranchePool.sol
    Lines covered: 219 / 422 (51.9%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    
                                                    
                                                
    4
    import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
    5
    import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    6
    
                                                    
                                                
    7
    contract TranchePool is Ownable {
    8
        using SafeERC20 for IERC20;
    9
    
                                                    
                                                
    10
        // Errors
    11
        error TranchePool__NotWhiteListed(address user);
    12
        error TranchePool__LessThanDepositThreshold(uint256 amount);
    13
        error TranchePool__InvalidAllocationRatio();
    14
        error TranchePool__InsufficientLiquidity();
    15
        error TranchePool__InsufficientShares();
    16
        error TranchePool__ZeroWithdrawal();
    17
        error TranchePool__NotWhiteListedForEquityTranche(address user);
    18
        error TranchePool__InvalidTransferAmount(uint256 amount);
    19
        error TranchePool__InvalidCaller(address user);
    20
        error TranchePool__ZeroAPRError();
    21
        error TranchePool__LossExceededCapital(uint256 remaining);
    22
        error TranchePool__ZeroSharesMinted();
    23
        error TranchePool__PoolIsNotOpen();
    24
        error TranchePool__InvalidStateTransition(PoolState state);
    25
        error TranchePool__WithdrawNotAllowed(PoolState state);
    26
        error TranchePool__ZeroValueError();
    27
        error TranchePool__MaxDepositCapExceeded(uint256 maxCap, uint256 amount);
    28
        error TranchePool__PoolIsNotCommited();
    29
        error TranchePool__PrincipalRepaymentExceeded();
    30
        error TranchePool__ZeroAddressError();
    31
        error TranchePool__DeployedCapitalExists();
    32
        error TranchePool__InvalidMaxCapAmount();
    33
        error TranchePool__InvalidMinDepositAmount();
    34
        error TranchePool__InterestNotClaimed();
    35
        // Events
    36
    
                                                    
                                                
    37
        event PoolStateUpdated(PoolState newState);
    38
    
                                                    
                                                
    39
        event LossAllocated(
    40
            uint256 seniorLoss,
    41
            uint256 juniorLoss,
    42
            uint256 equityLoss
    43
        );
    44
        event WithdrawnFromSeniorTranche(
    45
            address indexed user,
    46
            uint256 amount,
    47
            uint256 sharesBurned,
    48
            uint256 time
    49
        );
    50
        event WithdrawnFromJuniorTranche(
    51
            address indexed user,
    52
            uint256 amount,
    53
            uint256 sharesBurned,
    54
            uint256 time
    55
        );
    56
        event WithdrawnFromEquityTranche(
    57
            address indexed user,
    58
            uint256 amount,
    59
            uint256 sharesBurned,
    60
            uint256 time
    61
        );
    62
    
                                                    
                                                
    63
        event FundsDepositedToSeniorTranche(
    64
            address indexed user,
    65
            uint256 amount,
    66
            uint256 shares,
    67
            uint256 time
    68
        );
    69
        event FundsDepositedToJuniorTranche(
    70
            address indexed user,
    71
            uint256 amount,
    72
            uint256 shares,
    73
            uint256 time
    74
        );
    75
        event FundsDepositedToEquityTranche(
    76
            address indexed user,
    77
            uint256 amount,
    78
            uint256 shares,
    79
            uint256 time
    80
        );
    81
        event CapitalAllocated(
    82
            uint256 seniorAmount,
    83
            uint256 juniorAmount,
    84
            uint256 equityAmount,
    85
            uint256 time
    86
        );
    87
        event RecoverAmountTransferredToTranchePool(
    88
            uint256 amount,
    89
            uint256 timeStamp
    90
        );
    91
        event ProfitTransferredToTranchePool(uint256 amount, uint256 timeStamp);
    92
        event CapitalAllocationFactorUpdatedSenior(uint256 newFactor);
    93
        event CapitalAllocationFactorUpdatedJunior(uint256 newFactor);
    94
    
                                                    
                                                
    95
        enum PoolState {
    96
            OPEN, // deposits allowed
    97
            COMMITED,
    98
            DEPLOYED, // capital deployed, deposits paused
    99
            CLOSED // withdrawals only
    100
        }
    101
    
                                                    
                                                
    102
        // Whitelist
    103
        mapping(address => bool) public whiteListedLps;
    104
        mapping(address => bool) public whiteListedForEquityTranche;
    105
    
                                                    
                                                
    106
        // Shares tracking (instead of amounts)
    107
        mapping(address => uint256) public s_seniorTrancheShares;
    108
        mapping(address => uint256) public s_juniorTrancheShares;
    109
        mapping(address => uint256) public s_equityTrancheShares;
    110
    
                                                    
                                                
    111
        uint256 public s_totalSeniorShares;
    112
        uint256 public s_totalJuniorShares;
    113
        uint256 public s_totalEquityShares;
    114
    
                                                    
                                                
    115
        // Total value in each tranche (this decreases when capital is allocated)
    116
        // why some part of the capital should stay idle.
    117
        // 1. Liquidity and operational buffer.
    118
        //
    119
        uint256 public s_seniorTrancheIdleValue;
    120
        uint256 public s_juniorTrancheIdleValue;
    121
        uint256 public s_equityTrancheIdleValue;
    122
    
                                                    
                                                
    123
    ✓ 35.8K
        uint256 public s_seniorTrancheDeployedValue;
    124
        uint256 public s_juniorTrancheDeployedValue;
    125
        uint256 public s_equityTrancheDeployedValue;
    126
    
                                                    
                                                
    127
        // Minimum deposits
    128
    ✓ 58.3K
        uint256 public s_minimumDepositAmountSeniorTranche;
    129
        uint256 public s_minimumDepositAmountJuniorTranche;
    130
        uint256 public s_minimumDepositAmountEquityTranche;
    131
    
                                                    
                                                
    132
        // CHANGED: global interest index (scaled)
    133
        uint256 public seniorInterestIndex; // 1e18 precision
    134
        uint256 public juniorInterestIndex; // 1e18 precision
    135
        uint256 public equityInterestIndex; // 1e18 precision
    136
    
                                                    
                                                
    137
        // CHANGED: per-user last claimed index
    138
        mapping(address => uint256) public seniorUserIndex;
    139
        mapping(address => uint256) public juniorUserIndex;
    140
        mapping(address => uint256) public equityUserIndex;
    141
    
                                                    
                                                
    142
        // Stable coin
    143
        address public s_stableCoin;
    144
        address public loanEngine;
    145
    
                                                    
                                                
    146
        // Capital allocation factor (e.g., 80 for 80% senior, 15% junior, 5% equity)
    147
        uint256 public s_capital_allocation_factor_senior;
    148
        uint256 public s_capital_allocation_factor_junior;
    149
    
                                                    
                                                
    150
        uint256 public s_senior_apr;
    151
        uint256 public s_target_junior_apr;
    152
    
                                                    
                                                
    153
        uint256 public seniorAccruedInterest;
    154
        uint256 public juniorAccruedInterest;
    155
        uint256 public equityAccruedInterest;
    156
    
                                                    
                                                
    157
    ✓ 94.1K
        uint256 public s_seniorTrancheMaxCap;
    158
    ✓ 85.6K
        uint256 public s_juniorTrancheMaxCap;
    159
        uint256 public s_equityTrancheMaxCap;
    160
    
                                                    
                                                
    161
        uint256 public s_protocolRevenue;
    162
        uint256 public s_totalDeposited;
    163
        uint256 public s_totalLoss;
    164
        uint256 public s_totalRecovered;
    165
    
                                                    
                                                
    166
    ✓ 894.4K
        PoolState public poolState = PoolState.OPEN;
    167
    
                                                    
                                                
    168
        uint256 public seniorPrincipalShortfall;
    169
        uint256 public juniorPrincipalShortfall;
    170
        uint256 public equityPrincipalShortfall;
    171
    
                                                    
                                                
    172
        uint256 public s_totalUnclaimedInterest;
    173
    
                                                    
                                                
    174
        modifier isWhiteListed(address user) {
    175
    ✓ 22.5K
            _isWhiteListed(user);
    176
            _;
    177
        }
    178
    
                                                    
                                                
    179
        function _isWhiteListed(address user) internal view {
    180
    ✓ 44.8K
            if (!whiteListedLps[user]) {
    181
                revert TranchePool__NotWhiteListed(user);
    182
            }
    183
        }
    184
    
                                                    
                                                
    185
        modifier onlyLoanEngine(address user) {
    186
    ✓ 35.3K
            _onlyLoanEngine(user);
    187
    ✓ 17.1K
            _;
    188
        }
    189
    
                                                    
                                                
    190
        function _onlyLoanEngine(address user) internal view {
    191
    ✓ 69.6K
            if (user != loanEngine) {
    192
                revert TranchePool__InvalidCaller(user);
    193
            }
    194
        }
    195
    
                                                    
                                                
    196
        modifier isWhiteListedForEquityTranche(address user) {
    197
    ✓ 22.2K
            _isWhiteListedForEquityTranche(user);
    198
            _;
    199
        }
    200
    
                                                    
                                                
    201
        function _isWhiteListedForEquityTranche(address user) internal view {
    202
    ✓ 22.2K
            if (!whiteListedForEquityTranche[user]) {
    203
                revert TranchePool__NotWhiteListedForEquityTranche(user);
    204
            }
    205
        }
    206
    
                                                    
                                                
    207
    ✓ 1
        constructor(address stableCoin_) Ownable(msg.sender) {
    208
    ✓ 1
            s_stableCoin = stableCoin_;
    209
    ✓ 1
            seniorInterestIndex = 1e18;
    210
    ✓ 1
            juniorInterestIndex = 1e18;
    211
    ✓ 1
            equityInterestIndex = 1e18;
    212
        }
    213
    
                                                    
                                                
    214
        function depositSeniorTranche(
    215
            uint256 amount
    216
    ✓ 22.5K
        ) external isWhiteListed(msg.sender) {
    217
            // q: wy we need a minimum deposit for a tranche?
    218
            // a: in book.
    219
    ✓ 22.5K
            if (poolState != PoolState.OPEN) {
    220
                revert TranchePool__PoolIsNotOpen();
    221
            }
    222
    ✓ 22.5K
            if (amount == 0) {
    223
                revert TranchePool__ZeroValueError();
    224
            }
    225
    ✓ 22.5K
            if (amount < s_minimumDepositAmountSeniorTranche) {
    226
                revert TranchePool__LessThanDepositThreshold(amount);
    227
            }
    228
            // why there is a max cap exists?
    229
            //
    230
            //  1. to prevent the liquidity from sitting idle
    231
            //
    232
    ✓ 22.5K
            if (amount + s_seniorTrancheIdleValue > s_seniorTrancheMaxCap) {
    233
                revert TranchePool__MaxDepositCapExceeded(
    234
                    s_seniorTrancheMaxCap,
    235
                    amount
    236
                );
    237
            }
    238
    
                                                    
                                                
    239
            // Calculate shares to mint
    240
            // invariant: the total shares == idle value because the deposit is allowed only when
    241
            // the pool is open and once the pool is moved to a new state new deposits are not allowed
    242
            // so what shares == amount holding 1:1 is valid and is not affecting or opening any attack vectors.
    243
            uint256 shares = amount;
    244
    
                                                    
                                                
    245
    ✓ 22.5K
            IERC20(s_stableCoin).safeTransferFrom(
    246
                msg.sender,
    247
    ✓ 22.5K
                address(this),
    248
    ✓ 22.5K
                amount
    249
            );
    250
    
                                                    
                                                
    251
    ✓ 22.5K
            s_seniorTrancheShares[msg.sender] += shares;
    252
    ✓ 22.5K
            s_totalSeniorShares += shares;
    253
    ✓ 22.5K
            s_seniorTrancheIdleValue += amount;
    254
    ✓ 22.5K
            seniorUserIndex[msg.sender] = seniorInterestIndex;
    255
    ✓ 22.5K
            s_totalDeposited += amount;
    256
            emit FundsDepositedToSeniorTranche(
    257
                msg.sender,
    258
                amount,
    259
                shares,
    260
    ✓ 22.5K
                block.timestamp
    261
            );
    262
        }
    263
    
                                                    
                                                
    264
        function depositJuniorTranche(
    265
            uint256 amount
    266
    ✓ 22.3K
        ) external isWhiteListed(msg.sender) {
    267
    ✓ 22.3K
            if (poolState != PoolState.OPEN) {
    268
                revert TranchePool__PoolIsNotOpen();
    269
            }
    270
    ✓ 22.3K
            if (amount < s_minimumDepositAmountJuniorTranche) {
    271
                revert TranchePool__LessThanDepositThreshold(amount);
    272
            }
    273
    
                                                    
                                                
    274
    ✓ 22.3K
            if (amount + s_juniorTrancheIdleValue > s_juniorTrancheMaxCap) {
    275
                revert TranchePool__MaxDepositCapExceeded(
    276
                    s_juniorTrancheMaxCap,
    277
                    amount
    278
                );
    279
            }
    280
    
                                                    
                                                
    281
            uint256 shares = amount;
    282
    
                                                    
                                                
    283
    ✓ 22.3K
            IERC20(s_stableCoin).safeTransferFrom(
    284
                msg.sender,
    285
    ✓ 22.3K
                address(this),
    286
    ✓ 22.3K
                amount
    287
            );
    288
    
                                                    
                                                
    289
    ✓ 22.3K
            s_juniorTrancheShares[msg.sender] += shares;
    290
    ✓ 22.3K
            s_totalJuniorShares += shares;
    291
    ✓ 22.3K
            s_juniorTrancheIdleValue += amount;
    292
    ✓ 22.3K
            juniorUserIndex[msg.sender] = juniorInterestIndex;
    293
    ✓ 22.3K
            s_totalDeposited += amount;
    294
    
                                                    
                                                
    295
            emit FundsDepositedToJuniorTranche(
    296
                msg.sender,
    297
                amount,
    298
                shares,
    299
    ✓ 22.3K
                block.timestamp
    300
            );
    301
        }
    302
    
                                                    
                                                
    303
        function depositEquityTranche(
    304
            uint256 amount
    305
    ✓ 22.2K
        ) external isWhiteListedForEquityTranche(msg.sender) {
    306
    ✓ 22.2K
            if (poolState != PoolState.OPEN) {
    307
                revert TranchePool__PoolIsNotOpen();
    308
            }
    309
    ✓ 22.2K
            if (amount < s_minimumDepositAmountEquityTranche) {
    310
                revert TranchePool__LessThanDepositThreshold(amount);
    311
            }
    312
    
                                                    
                                                
    313
    ✓ 22.2K
            if (amount + s_equityTrancheIdleValue > s_equityTrancheMaxCap) {
    314
                revert TranchePool__MaxDepositCapExceeded(
    315
                    s_equityTrancheMaxCap,
    316
                    amount
    317
                );
    318
            }
    319
    
                                                    
                                                
    320
            uint256 shares = amount;
    321
    
                                                    
                                                
    322
    ✓ 22.2K
            IERC20(s_stableCoin).safeTransferFrom(
    323
                msg.sender,
    324
    ✓ 22.2K
                address(this),
    325
    ✓ 22.2K
                amount
    326
            );
    327
    ✓ 22.2K
            s_equityTrancheShares[msg.sender] += shares;
    328
    ✓ 22.2K
            s_totalEquityShares += shares;
    329
    ✓ 22.2K
            s_equityTrancheIdleValue += amount;
    330
    ✓ 22.2K
            equityUserIndex[msg.sender] = equityInterestIndex;
    331
    ✓ 22.2K
            s_totalDeposited += amount;
    332
    
                                                    
                                                
    333
            emit FundsDepositedToEquityTranche(
    334
                msg.sender,
    335
                amount,
    336
                shares,
    337
    ✓ 22.2K
                block.timestamp
    338
            );
    339
        }
    340
    
                                                    
                                                
    341
        /**
    342
         * @notice Allocate capital according to the 80/20 split
    343
         * @param totalDisbursement Total amount to allocate from the pool
    344
         * @param fees Total fees to be collected
    345
         */
    346
        function allocateCapital(
    347
            uint256 totalDisbursement,
    348
            uint256 fees,
    349
            address deployer,
    350
            address feeManager
    351
    ✓ 35.3K
        ) external onlyLoanEngine(msg.sender) returns (uint256, uint256, uint256) {
    352
            if (
    353
    ✓ 35.3K
                poolState != PoolState.COMMITED && poolState != PoolState.DEPLOYED
    354
            ) {
    355
                revert TranchePool__PoolIsNotCommited();
    356
            }
    357
    
                                                    
                                                
    358
    ✓ 35.3K
            uint256 totalAmount = totalDisbursement + fees;
    359
    
                                                    
                                                
    360
            // Global liquidity check
    361
    ✓ 35.3K
            uint256 totalIdle = s_seniorTrancheIdleValue +
    362
    ✓ 35.3K
                s_juniorTrancheIdleValue +
    363
    ✓ 35.3K
                s_equityTrancheIdleValue;
    364
    
                                                    
                                                
    365
    ✓ 35.3K
            if (totalAmount > totalIdle) {
    366
                revert TranchePool__InsufficientLiquidity();
    367
            }
    368
    
                                                    
                                                
    369
            uint256 targetSenior = (totalAmount *
    370
    ✓ 35.3K
                s_capital_allocation_factor_senior) / 100;
    371
    
                                                    
                                                
    372
            uint256 targetJunior = (totalAmount *
    373
    ✓ 35.3K
                s_capital_allocation_factor_junior) / 100;
    374
    
                                                    
                                                
    375
    ✓ 35.3K
            uint256 targetEquity = totalAmount - targetSenior - targetJunior;
    376
    
                                                    
                                                
    377
    ✓ 35.3K
            uint256 seniorAmount = _minimum(targetSenior, s_seniorTrancheIdleValue);
    378
    
                                                    
                                                
    379
    ✓ 35.3K
            uint256 juniorAmount = _minimum(targetJunior, s_juniorTrancheIdleValue);
    380
    
                                                    
                                                
    381
    ✓ 35.3K
            uint256 equityAmount = _minimum(targetEquity, s_equityTrancheIdleValue);
    382
    
                                                    
                                                
    383
    ✓ 35.3K
            uint256 allocated = seniorAmount + juniorAmount + equityAmount;
    384
    
                                                    
                                                
    385
    ✓ 35.3K
            uint256 remaining = totalAmount - allocated;
    386
    
                                                    
                                                
    387
            // Equity absorbs first
    388
    ✓ 35.3K
            if (remaining > 0 && s_equityTrancheIdleValue > equityAmount) {
    389
                uint256 extra = _minimum(
    390
                    remaining,
    391
    ✓ 8.2K
                    s_equityTrancheIdleValue - equityAmount
    392
                );
    393
    ✓ 8.2K
                equityAmount += extra;
    394
    ✓ 8.2K
                remaining -= extra;
    395
            }
    396
    
                                                    
                                                
    397
            // Junior absorbs next
    398
    ✓ 35.3K
            if (remaining > 0 && s_juniorTrancheIdleValue > juniorAmount) {
    399
                uint256 extra = _minimum(
    400
                    remaining,
    401
    ✓ 8.2K
                    s_juniorTrancheIdleValue - juniorAmount
    402
                );
    403
    ✓ 8.2K
                juniorAmount += extra;
    404
    ✓ 8.2K
                remaining -= extra;
    405
            }
    406
    
                                                    
                                                
    407
            // Senior absorbs last
    408
    ✓ 35.3K
            if (remaining > 0 && s_seniorTrancheIdleValue > seniorAmount) {
    409
                uint256 extra = _minimum(
    410
                    remaining,
    411
    ✓ 10.5K
                    s_seniorTrancheIdleValue - seniorAmount
    412
                );
    413
    ✓ 18.7K
                seniorAmount += extra;
    414
    ✓ 10.5K
                remaining -= extra;
    415
            }
    416
    
                                                    
                                                
    417
            // Final safety check
    418
    ✓ 35.3K
            if (remaining > 0) {
    419
                revert TranchePool__InsufficientLiquidity();
    420
            }
    421
    
                                                    
                                                
    422
            if (poolState == PoolState.COMMITED) {
    423
    ✓ 21.2K
                poolState = PoolState.DEPLOYED;
    424
    ✓ 21.2K
                emit PoolStateUpdated(PoolState.DEPLOYED);
    425
            }
    426
    
                                                    
                                                
    427
    ✓ 35.3K
            s_seniorTrancheIdleValue -= seniorAmount;
    428
    ✓ 35.3K
            s_juniorTrancheIdleValue -= juniorAmount;
    429
    ✓ 35.3K
            s_equityTrancheIdleValue -= equityAmount;
    430
    
                                                    
                                                
    431
    ✓ 35.3K
            s_seniorTrancheDeployedValue += seniorAmount;
    432
    ✓ 35.3K
            s_juniorTrancheDeployedValue += juniorAmount;
    433
    ✓ 35.3K
            s_equityTrancheDeployedValue += equityAmount;
    434
    
                                                    
                                                
    435
    ✓ 35.3K
            IERC20(s_stableCoin).safeTransfer(deployer, totalDisbursement);
    436
    
                                                    
                                                
    437
    ✓ 35.3K
            if (fees > 0) {
    438
    ✓ 35.3K
                IERC20(s_stableCoin).safeTransfer(feeManager, fees);
    439
            }
    440
    
                                                    
                                                
    441
            emit CapitalAllocated(
    442
                seniorAmount,
    443
                juniorAmount,
    444
                equityAmount,
    445
    ✓ 35.3K
                block.timestamp
    446
            );
    447
            return (seniorAmount, juniorAmount, equityAmount);
    448
        }
    449
    
                                                    
                                                
    450
        function onInterestAccrued(
    451
            uint256 interestAmount,
    452
            uint256 seniorInterest,
    453
            uint256 juniorInterest
    454
    ✓ 17.1K
        ) external onlyLoanEngine(msg.sender) {
    455
    ✓ 17.1K
            if (interestAmount == 0) return;
    456
    
                                                    
                                                
    457
    ✓ 17.1K
            seniorAccruedInterest += seniorInterest;
    458
    ✓ 17.1K
            juniorAccruedInterest += juniorInterest;
    459
    ✓ 17.1K
            equityAccruedInterest += (interestAmount -
    460
                seniorInterest -
    461
                juniorInterest);
    462
        }
    463
    
                                                    
                                                
    464
        function onRepayment(
    465
            uint256 principalRepaid,
    466
            uint256 interestRepaid
    467
    ✓ 17.2K
        ) external onlyLoanEngine(msg.sender) {
    468
    ✓ 17.2K
            if (principalRepaid == 0 && interestRepaid == 0) {
    469
                revert TranchePool__InvalidTransferAmount(0);
    470
            }
    471
    
                                                    
                                                
    472
            /*//////////////////////////////////////////////////////////////
    473
                            INTEREST WATERFALL (INDEXED)
    474
        //////////////////////////////////////////////////////////////*/
    475
    
                                                    
                                                
    476
    ✓ 17.2K
            uint256 remainingInterest = interestRepaid;
    477
    ✓ 17.2K
            s_totalUnclaimedInterest += interestRepaid;
    478
    
                                                    
                                                
    479
            // 1️⃣ Senior interest
    480
            if (
    481
    ✓ 17.2K
                remainingInterest > 0 &&
    482
    ✓ 17.1K
                seniorAccruedInterest > 0 &&
    483
    ✓ 10.9K
                s_totalSeniorShares > 0
    484
            ) {
    485
                uint256 seniorPaid = _minimum(
    486
                    remainingInterest,
    487
    ✓ 10.9K
                    seniorAccruedInterest
    488
                );
    489
    ✓ 10.9K
                seniorAccruedInterest -= seniorPaid;
    490
    ✓ 10.9K
                seniorInterestIndex += (seniorPaid * 1e18) / s_totalSeniorShares;
    491
    ✓ 10.9K
                remainingInterest -= seniorPaid;
    492
            }
    493
    
                                                    
                                                
    494
            // 2️⃣ Junior interest
    495
            if (
    496
    ✓ 17.2K
                remainingInterest > 0 &&
    497
    ✓ 15.5K
                juniorAccruedInterest > 0 &&
    498
    ✓ 12.6K
                s_totalJuniorShares > 0
    499
            ) {
    500
                uint256 juniorPaid = _minimum(
    501
                    remainingInterest,
    502
    ✓ 12.6K
                    juniorAccruedInterest
    503
                );
    504
    ✓ 23.5K
                juniorAccruedInterest -= juniorPaid;
    505
    ✓ 12.6K
                juniorInterestIndex += (juniorPaid * 1e18) / s_totalJuniorShares;
    506
    ✓ 12.6K
                remainingInterest -= juniorPaid;
    507
            }
    508
    
                                                    
                                                
    509
            // 3️⃣ Equity / overflow interest
    510
    ✓ 17.2K
            if (remainingInterest > 0) {
    511
    ✓ 12.3K
                if (s_totalEquityShares > 0) {
    512
                    equityInterestIndex +=
    513
    ✓ 11.6K
                        (remainingInterest * 1e18) /
    514
                        s_totalEquityShares;
    515
                    equityAccruedInterest -= _minimum(
    516
    ✓ 11.6K
                        equityAccruedInterest,
    517
                        remainingInterest
    518
                    );
    519
    ✓ 619
                } else if (s_totalJuniorShares > 0) {
    520
                    // no equity → junior gets excess
    521
                    juniorInterestIndex +=
    522
    ✓ 619
                        (remainingInterest * 1e18) /
    523
                        s_totalJuniorShares;
    524
                } else {
    525
                    // no LPs left → protocol revenue
    526
                    s_protocolRevenue += remainingInterest;
    527
                }
    528
            }
    529
    
                                                    
                                                
    530
            /*//////////////////////////////////////////////////////////////
    531
                            PRINCIPAL REDEMPTION
    532
                (REVERSE OF LOSS WATERFALL — NO RATIOS)
    533
        //////////////////////////////////////////////////////////////*/
    534
    
                                                    
                                                
    535
    ✓ 17.2K
            if (principalRepaid > 0) {
    536
    ✓ 16.3K
                uint256 remaining = principalRepaid;
    537
    
                                                    
                                                
    538
                // Senior first (restore safest capital)
    539
    ✓ 16.3K
                if (remaining > 0 && s_seniorTrancheDeployedValue > 0) {
    540
                    uint256 seniorPay = _minimum(
    541
                        remaining,
    542
    ✓ 10.2K
                        s_seniorTrancheDeployedValue
    543
                    );
    544
    ✓ 10.2K
                    s_seniorTrancheDeployedValue -= seniorPay;
    545
    ✓ 10.2K
                    s_seniorTrancheIdleValue += seniorPay;
    546
    ✓ 10.2K
                    remaining -= seniorPay;
    547
                }
    548
    
                                                    
                                                
    549
                // Junior next
    550
    ✓ 16.3K
                if (remaining > 0 && s_juniorTrancheDeployedValue > 0) {
    551
                    uint256 juniorPay = _minimum(
    552
                        remaining,
    553
    ✓ 8.6K
                        s_juniorTrancheDeployedValue
    554
                    );
    555
    ✓ 8.6K
                    s_juniorTrancheDeployedValue -= juniorPay;
    556
    ✓ 8.6K
                    s_juniorTrancheIdleValue += juniorPay;
    557
    ✓ 8.6K
                    remaining -= juniorPay;
    558
                }
    559
    
                                                    
                                                
    560
                // Equity last
    561
    ✓ 16.3K
                if (remaining > 0 && s_equityTrancheDeployedValue > 0) {
    562
                    uint256 equityPay = _minimum(
    563
                        remaining,
    564
    ✓ 7.7K
                        s_equityTrancheDeployedValue
    565
                    );
    566
    ✓ 26.4K
                    s_equityTrancheDeployedValue -= equityPay;
    567
    ✓ 7.7K
                    s_equityTrancheIdleValue += equityPay;
    568
    ✓ 7.7K
                    remaining -= equityPay;
    569
                }
    570
    
                                                    
                                                
    571
                // Safety: should never happen unless LoanEngine lies
    572
                if (remaining > 0) {
    573
                    revert TranchePool__PrincipalRepaymentExceeded();
    574
                }
    575
            }
    576
        }
    577
    
                                                    
                                                
    578
        // lp profit withdrawal is pending but it can only be implemented
    579
        // after the loan enginge implementation which determines how the
    580
        // interest will be accured and the distribution is dependent on the
    581
        // share capacity
    582
    
                                                    
                                                
    583
        function onLoss(
    584
            uint256 principalLoss,
    585
            uint256 interestAccrued
    586
    ✓ 5
        ) external onlyLoanEngine(msg.sender) {
    587
    ✓ 5
            if (principalLoss == 0 && interestAccrued == 0) {
    588
                revert TranchePool__ZeroValueError();
    589
            }
    590
    
                                                    
                                                
    591
            /*//////////////////////////////////////////////////////////////
    592
                        1️⃣ CANCEL GHOST INTEREST
    593
            (SAME PRIORITY AS INTEREST PAYOUT)
    594
        //////////////////////////////////////////////////////////////*/
    595
    
                                                    
                                                
    596
    ✓ 5
            uint256 remainingInterest = interestAccrued;
    597
    
                                                    
                                                
    598
            // Cancel senior accrued interest first
    599
    ✓ 5
            if (remainingInterest > 0 && seniorAccruedInterest > 0) {
    600
                uint256 seniorCancel = _minimum(
    601
                    remainingInterest,
    602
    ✓ 5
                    seniorAccruedInterest
    603
                );
    604
    ✓ 5
                seniorAccruedInterest -= seniorCancel;
    605
    ✓ 5
                remainingInterest -= seniorCancel;
    606
            }
    607
    
                                                    
                                                
    608
            // Then junior
    609
    ✓ 5
            if (remainingInterest > 0 && juniorAccruedInterest > 0) {
    610
                uint256 juniorCancel = _minimum(
    611
                    remainingInterest,
    612
    ✓ 17.1K
                    juniorAccruedInterest
    613
                );
    614
    ✓ 17.1K
                juniorAccruedInterest -= juniorCancel;
    615
    ✓ 17.1K
                remainingInterest -= juniorCancel;
    616
            }
    617
    
                                                    
                                                
    618
            // Any remaining interest is ignored (equity / protocol had no promise)
    619
    
                                                    
                                                
    620
            /*//////////////////////////////////////////////////////////////
    621
                        2️⃣ PRINCIPAL LOSS WATERFALL
    622
                    Equity → Junior → Senior
    623
        //////////////////////////////////////////////////////////////*/
    624
    
                                                    
                                                
    625
    ✓ 5
            s_totalLoss += principalLoss;
    626
    ✓ 5
            uint256 remaining = principalLoss;
    627
    
                                                    
                                                
    628
    ✓ 5
            uint256 equityLoss;
    629
    ✓ 5
            uint256 juniorLoss;
    630
    ✓ 5
            uint256 seniorLoss;
    631
    
                                                    
                                                
    632
            // Equity absorbs first
    633
    ✓ 5
            if (remaining > 0 && s_equityTrancheDeployedValue > 0) {
    634
    ✓ 5
                equityLoss = _minimum(remaining, s_equityTrancheDeployedValue);
    635
    ✓ 5
                s_equityTrancheDeployedValue -= equityLoss;
    636
    ✓ 5
                equityPrincipalShortfall += equityLoss;
    637
    ✓ 5
                remaining -= equityLoss;
    638
            }
    639
    
                                                    
                                                
    640
            // Junior next
    641
    ✓ 5
            if (remaining > 0 && s_juniorTrancheDeployedValue > 0) {
    642
                juniorLoss = _minimum(remaining, s_juniorTrancheDeployedValue);
    643
                s_juniorTrancheDeployedValue -= juniorLoss;
    644
                juniorPrincipalShortfall += juniorLoss;
    645
                remaining -= juniorLoss;
    646
            }
    647
    
                                                    
                                                
    648
            // Senior last
    649
    ✓ 5
            if (remaining > 0 && s_seniorTrancheDeployedValue > 0) {
    650
                seniorLoss = _minimum(remaining, s_seniorTrancheDeployedValue);
    651
                s_seniorTrancheDeployedValue -= seniorLoss;
    652
                seniorPrincipalShortfall += seniorLoss;
    653
                remaining -= seniorLoss;
    654
            }
    655
    
                                                    
                                                
    656
    ✓ 5
            if (remaining > 0) {
    657
                revert TranchePool__LossExceededCapital(remaining);
    658
            }
    659
    
                                                    
                                                
    660
    ✓ 29.6K
            emit LossAllocated(seniorLoss, juniorLoss, equityLoss);
    661
        }
    662
    
                                                    
                                                
    663
        // on recovery what happens is the protocol may recover more than he lost and it can cause appreciation of the share value when withdrawing, keeping the design simple because adding it to interest accured make no difference at the end of withdrawing.
    664
    
                                                    
                                                
    665
        function onRecovery(uint256 amount) external onlyLoanEngine(msg.sender) {
    666
            if (amount == 0) {
    667
                revert TranchePool__ZeroValueError();
    668
            }
    669
    
                                                    
                                                
    670
            s_totalRecovered += amount;
    671
            uint256 remaining = amount;
    672
    
                                                    
                                                
    673
            // Senior first
    674
            if (remaining > 0 && seniorPrincipalShortfall > 0) {
    675
                uint256 seniorPay = _minimum(remaining, seniorPrincipalShortfall);
    676
                seniorPrincipalShortfall -= seniorPay;
    677
                s_seniorTrancheIdleValue += seniorPay;
    678
                remaining -= seniorPay;
    679
            }
    680
    
                                                    
                                                
    681
            // Junior next
    682
            if (remaining > 0 && juniorPrincipalShortfall > 0) {
    683
                uint256 juniorPay = _minimum(remaining, juniorPrincipalShortfall);
    684
                juniorPrincipalShortfall -= juniorPay;
    685
                s_juniorTrancheIdleValue += juniorPay;
    686
                remaining -= juniorPay;
    687
            }
    688
    
                                                    
                                                
    689
            // Equity last
    690
            if (remaining > 0 && equityPrincipalShortfall > 0) {
    691
                uint256 equityPay = _minimum(remaining, equityPrincipalShortfall);
    692
                equityPrincipalShortfall -= equityPay;
    693
                s_equityTrancheIdleValue += equityPay;
    694
                remaining -= equityPay;
    695
            }
    696
    
                                                    
                                                
    697
            // Any excess is true upside → equity
    698
            if (remaining > 0) {
    699
                s_equityTrancheIdleValue += remaining;
    700
            }
    701
    
                                                    
                                                
    702
            emit RecoverAmountTransferredToTranchePool(amount, block.timestamp);
    703
        }
    704
    
                                                    
                                                
    705
        // when the pool closes if the user withdraw the shares before claiming interest on those he will lose the interest for the withdrawn shares
    706
        function claimSeniorInterest() external {
    707
            uint256 userShares = s_seniorTrancheShares[msg.sender];
    708
            if (userShares == 0) revert TranchePool__InsufficientShares();
    709
    
                                                    
                                                
    710
            uint256 indexDelta = seniorInterestIndex - seniorUserIndex[msg.sender];
    711
    
                                                    
                                                
    712
            if (indexDelta == 0) revert TranchePool__ZeroWithdrawal();
    713
    
                                                    
                                                
    714
            uint256 claimable = (userShares * indexDelta) / 1e18;
    715
    
                                                    
                                                
    716
            // CHANGED: update user index BEFORE transfer
    717
            seniorUserIndex[msg.sender] = seniorInterestIndex;
    718
            s_totalUnclaimedInterest -= claimable;
    719
    
                                                    
                                                
    720
            IERC20(s_stableCoin).safeTransfer(msg.sender, claimable);
    721
        }
    722
    
                                                    
                                                
    723
        function claimJuniorInterest() external {
    724
            uint256 userShares = s_juniorTrancheShares[msg.sender];
    725
            if (userShares == 0) revert TranchePool__InsufficientShares();
    726
    
                                                    
                                                
    727
            uint256 indexDelta = juniorInterestIndex - juniorUserIndex[msg.sender];
    728
    
                                                    
                                                
    729
            if (indexDelta == 0) revert TranchePool__ZeroWithdrawal();
    730
    
                                                    
                                                
    731
            uint256 claimable = (userShares * indexDelta) / 1e18;
    732
    
                                                    
                                                
    733
            // CHANGED: update user index BEFORE transfer
    734
            juniorUserIndex[msg.sender] = juniorInterestIndex;
    735
            s_totalUnclaimedInterest -= claimable;
    736
    
                                                    
                                                
    737
            IERC20(s_stableCoin).safeTransfer(msg.sender, claimable);
    738
        }
    739
    
                                                    
                                                
    740
        function claimEquityInterest()
    741
            external
    742
            isWhiteListedForEquityTranche(msg.sender)
    743
        {
    744
            uint256 userShares = s_equityTrancheShares[msg.sender];
    745
            if (userShares == 0) revert TranchePool__InsufficientShares();
    746
    
                                                    
                                                
    747
            uint256 indexDelta = equityInterestIndex - equityUserIndex[msg.sender];
    748
    
                                                    
                                                
    749
            if (indexDelta == 0) revert TranchePool__ZeroWithdrawal();
    750
    
                                                    
                                                
    751
            uint256 claimable = (userShares * indexDelta) / 1e18;
    752
            s_totalUnclaimedInterest -= claimable;
    753
    
                                                    
                                                
    754
            // CHANGED: update user index BEFORE transfer
    755
            equityUserIndex[msg.sender] = equityInterestIndex;
    756
    
                                                    
                                                
    757
    ✓ 17.2K
            IERC20(s_stableCoin).safeTransfer(msg.sender, claimable);
    758
        }
    759
    
                                                    
                                                
    760
        /**
    761
         *
    762
         *
    763
         * @notice Withdraw from senior tranche by burning shares
    764
         * @param shares Number of shares to burn (0 = withdraw all)
    765
         * passing zero amount will cause the burn of all shares
    766
         */
    767
        function withdrawSeniorTranche(
    768
            uint256 shares
    769
        ) external isWhiteListed(msg.sender) {
    770
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    771
                revert TranchePool__WithdrawNotAllowed(poolState);
    772
            }
    773
            uint256 userShares = s_seniorTrancheShares[msg.sender];
    774
    
                                                    
                                                
    775
            if (userShares == 0) {
    776
                revert TranchePool__InsufficientShares();
    777
            }
    778
    
                                                    
                                                
    779
            // If shares is 0, withdraw everything
    780
            uint256 sharesToBurn = shares == 0 ? userShares : shares;
    781
    
                                                    
                                                
    782
            if (sharesToBurn > userShares) {
    783
                revert TranchePool__InsufficientShares();
    784
            }
    785
    
                                                    
                                                
    786
            // Calculate amount to withdraw based on current pool value
    787
            uint256 amountToWithdraw = (sharesToBurn * s_seniorTrancheIdleValue) /
    788
                s_totalSeniorShares;
    789
    
                                                    
                                                
    790
            if (seniorUserIndex[msg.sender] != seniorInterestIndex) {
    791
                revert TranchePool__InterestNotClaimed();
    792
            }
    793
    
                                                    
                                                
    794
            if (amountToWithdraw == 0) {
    795
                revert TranchePool__ZeroWithdrawal();
    796
            }
    797
    
                                                    
                                                
    798
            if (amountToWithdraw > s_seniorTrancheIdleValue) {
    799
                revert TranchePool__InsufficientLiquidity();
    800
            }
    801
    
                                                    
                                                
    802
            // Update state before transfer (CEI pattern)
    803
            s_seniorTrancheShares[msg.sender] -= sharesToBurn;
    804
            s_totalSeniorShares -= sharesToBurn;
    805
            s_seniorTrancheIdleValue -= amountToWithdraw;
    806
            s_totalDeposited -= amountToWithdraw;
    807
            // Transfer tokens
    808
            IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw);
    809
    
                                                    
                                                
    810
            emit WithdrawnFromSeniorTranche(
    811
                msg.sender,
    812
                amountToWithdraw,
    813
                sharesToBurn,
    814
                block.timestamp
    815
            );
    816
        }
    817
    
                                                    
                                                
    818
        /**
    819
         * @notice Withdraw from junior tranche by burning shares
    820
         * @param shares Number of shares to burn (0 = withdraw all)
    821
         */
    822
        function withdrawJuniorTranche(
    823
            uint256 shares
    824
        ) external isWhiteListed(msg.sender) {
    825
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    826
                revert TranchePool__WithdrawNotAllowed(poolState);
    827
            }
    828
            uint256 userShares = s_juniorTrancheShares[msg.sender];
    829
    
                                                    
                                                
    830
            if (userShares == 0) {
    831
                revert TranchePool__InsufficientShares();
    832
            }
    833
    
                                                    
                                                
    834
            if (juniorUserIndex[msg.sender] != juniorInterestIndex) {
    835
                revert TranchePool__InterestNotClaimed();
    836
            }
    837
    
                                                    
                                                
    838
            uint256 sharesToBurn = shares == 0 ? userShares : shares;
    839
    
                                                    
                                                
    840
            if (sharesToBurn > userShares) {
    841
                revert TranchePool__InsufficientShares();
    842
            }
    843
    
                                                    
                                                
    844
            uint256 amountToWithdraw = (sharesToBurn * s_juniorTrancheIdleValue) /
    845
                s_totalJuniorShares;
    846
    
                                                    
                                                
    847
            if (amountToWithdraw == 0) {
    848
                revert TranchePool__ZeroWithdrawal();
    849
            }
    850
            if (amountToWithdraw > s_juniorTrancheIdleValue) {
    851
                revert TranchePool__InsufficientLiquidity();
    852
            }
    853
    
                                                    
                                                
    854
            s_juniorTrancheShares[msg.sender] -= sharesToBurn;
    855
            s_totalJuniorShares -= sharesToBurn;
    856
            s_juniorTrancheIdleValue -= amountToWithdraw;
    857
            s_totalDeposited -= amountToWithdraw;
    858
    
                                                    
                                                
    859
            IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw);
    860
    
                                                    
                                                
    861
            emit WithdrawnFromJuniorTranche(
    862
                msg.sender,
    863
                amountToWithdraw,
    864
                sharesToBurn,
    865
                block.timestamp
    866
            );
    867
        }
    868
    
                                                    
                                                
    869
        function withdrawEquityTranche(
    870
            uint256 shares
    871
        ) external isWhiteListedForEquityTranche(msg.sender) {
    872
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    873
                revert TranchePool__WithdrawNotAllowed(poolState);
    874
            }
    875
            uint256 userShares = s_equityTrancheShares[msg.sender];
    876
    
                                                    
                                                
    877
            if (userShares == 0) {
    878
                revert TranchePool__InsufficientShares();
    879
            }
    880
            if (equityUserIndex[msg.sender] != equityInterestIndex) {
    881
                revert TranchePool__InterestNotClaimed();
    882
            }
    883
            uint256 sharesToBurn = shares == 0 ? userShares : shares;
    884
            if (sharesToBurn > userShares) {
    885
                revert TranchePool__InsufficientShares();
    886
            }
    887
    
                                                    
                                                
    888
            uint256 amountToWithdraw = (sharesToBurn * s_equityTrancheIdleValue) /
    889
                s_totalEquityShares;
    890
    
                                                    
                                                
    891
            if (amountToWithdraw == 0) {
    892
                revert TranchePool__ZeroWithdrawal();
    893
            }
    894
            if (amountToWithdraw > s_equityTrancheIdleValue) {
    895
                revert TranchePool__InsufficientLiquidity();
    896
            }
    897
            s_equityTrancheShares[msg.sender] -= sharesToBurn;
    898
            s_totalEquityShares -= sharesToBurn;
    899
            s_equityTrancheIdleValue -= amountToWithdraw;
    900
            s_totalDeposited -= amountToWithdraw;
    901
    
                                                    
                                                
    902
            IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw);
    903
    
                                                    
                                                
    904
            emit WithdrawnFromEquityTranche(
    905
                msg.sender,
    906
                amountToWithdraw,
    907
                sharesToBurn,
    908
                block.timestamp
    909
            );
    910
        }
    911
    
                                                    
                                                
    912
        /**
    913
         * @notice Withdraw specific amount from senior tranche
    914
         * @param amount Amount of tokens to withdraw
    915
         */
    916
        function withdrawSeniorTrancheByAmount(
    917
            uint256 amount
    918
        ) external isWhiteListed(msg.sender) {
    919
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    920
                revert TranchePool__WithdrawNotAllowed(poolState);
    921
            }
    922
            if (amount == 0) {
    923
                revert TranchePool__ZeroWithdrawal();
    924
            }
    925
            if (seniorUserIndex[msg.sender] != seniorInterestIndex) {
    926
                revert TranchePool__InterestNotClaimed();
    927
            }
    928
    
                                                    
                                                
    929
            uint256 userBalance = getSeniorTrancheBalance(msg.sender);
    930
    
                                                    
                                                
    931
            if (amount > userBalance) {
    932
                revert TranchePool__InsufficientShares();
    933
            }
    934
    
                                                    
                                                
    935
            if (amount > s_seniorTrancheIdleValue) {
    936
                revert TranchePool__InsufficientLiquidity();
    937
            }
    938
    
                                                    
                                                
    939
            // Calculate shares to burn for this amount
    940
            uint256 sharesToBurn = (amount * s_totalSeniorShares) /
    941
                (s_seniorTrancheIdleValue);
    942
    
                                                    
                                                
    943
            // Handle rounding - ensure we don't try to withdraw more than available
    944
            if (sharesToBurn > s_seniorTrancheShares[msg.sender]) {
    945
                sharesToBurn = s_seniorTrancheShares[msg.sender];
    946
            }
    947
    
                                                    
                                                
    948
            s_seniorTrancheShares[msg.sender] -= sharesToBurn;
    949
            s_totalSeniorShares -= sharesToBurn;
    950
            s_seniorTrancheIdleValue -= amount;
    951
            s_totalDeposited -= amount;
    952
    
                                                    
                                                
    953
            IERC20(s_stableCoin).safeTransfer(msg.sender, amount);
    954
    
                                                    
                                                
    955
            emit WithdrawnFromSeniorTranche(
    956
                msg.sender,
    957
                amount,
    958
                sharesToBurn,
    959
                block.timestamp
    960
            );
    961
        }
    962
    
                                                    
                                                
    963
        /**
    964
         * @notice Withdraw specific amount from junior tranche
    965
         * @param amount Amount of tokens to withdraw
    966
         */
    967
        function withdrawJuniorTrancheByAmount(
    968
            uint256 amount
    969
        ) external isWhiteListed(msg.sender) {
    970
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    971
                revert TranchePool__WithdrawNotAllowed(poolState);
    972
            }
    973
            if (amount == 0) {
    974
                revert TranchePool__ZeroWithdrawal();
    975
            }
    976
    
                                                    
                                                
    977
            if (juniorUserIndex[msg.sender] != juniorInterestIndex) {
    978
                revert TranchePool__InterestNotClaimed();
    979
            }
    980
    
                                                    
                                                
    981
            uint256 userBalance = getJuniorTrancheBalance(msg.sender);
    982
    
                                                    
                                                
    983
            if (amount > userBalance) {
    984
                revert TranchePool__InsufficientShares();
    985
            }
    986
    
                                                    
                                                
    987
            if (amount > s_juniorTrancheIdleValue) {
    988
                revert TranchePool__InsufficientLiquidity();
    989
            }
    990
    
                                                    
                                                
    991
            uint256 sharesToBurn = (amount * s_totalJuniorShares) /
    992
                (s_juniorTrancheIdleValue);
    993
    
                                                    
                                                
    994
            if (sharesToBurn > s_juniorTrancheShares[msg.sender]) {
    995
                sharesToBurn = s_juniorTrancheShares[msg.sender];
    996
            }
    997
    
                                                    
                                                
    998
            s_juniorTrancheShares[msg.sender] -= sharesToBurn;
    999
            s_totalJuniorShares -= sharesToBurn;
    1000
            s_juniorTrancheIdleValue -= amount;
    1001
            s_totalDeposited -= amount;
    1002
    
                                                    
                                                
    1003
            IERC20(s_stableCoin).safeTransfer(msg.sender, amount);
    1004
    
                                                    
                                                
    1005
            emit WithdrawnFromJuniorTranche(
    1006
                msg.sender,
    1007
                amount,
    1008
                sharesToBurn,
    1009
                block.timestamp
    1010
            );
    1011
        }
    1012
    
                                                    
                                                
    1013
        function withdrawEquityTrancheByAmount(
    1014
            uint256 amount
    1015
        ) external isWhiteListedForEquityTranche(msg.sender) {
    1016
            if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
    1017
                revert TranchePool__WithdrawNotAllowed(poolState);
    1018
            }
    1019
            if (amount == 0) {
    1020
                revert TranchePool__ZeroWithdrawal();
    1021
            }
    1022
    
                                                    
                                                
    1023
            if (equityUserIndex[msg.sender] != equityInterestIndex) {
    1024
                revert TranchePool__InterestNotClaimed();
    1025
            }
    1026
    
                                                    
                                                
    1027
            uint256 userBalance = getEquityTrancheBalance(msg.sender);
    1028
    
                                                    
                                                
    1029
            if (amount > userBalance) {
    1030
                revert TranchePool__InsufficientShares();
    1031
            }
    1032
    
                                                    
                                                
    1033
            if (amount > s_equityTrancheIdleValue) {
    1034
                revert TranchePool__InsufficientLiquidity();
    1035
            }
    1036
    
                                                    
                                                
    1037
            uint256 sharesToBurn = (amount * s_totalEquityShares) /
    1038
                (s_equityTrancheIdleValue);
    1039
    
                                                    
                                                
    1040
            if (sharesToBurn > s_equityTrancheShares[msg.sender]) {
    1041
                sharesToBurn = s_equityTrancheShares[msg.sender];
    1042
            }
    1043
    
                                                    
                                                
    1044
            s_equityTrancheShares[msg.sender] -= sharesToBurn;
    1045
            s_totalEquityShares -= sharesToBurn;
    1046
            s_equityTrancheIdleValue -= amount;
    1047
            s_totalDeposited -= amount;
    1048
    
                                                    
                                                
    1049
            IERC20(s_stableCoin).safeTransfer(msg.sender, amount);
    1050
    
                                                    
                                                
    1051
            emit WithdrawnFromEquityTranche(
    1052
                msg.sender,
    1053
                amount,
    1054
                sharesToBurn,
    1055
                block.timestamp
    1056
            );
    1057
        }
    1058
    
                                                    
                                                
    1059
        /**
    1060
         * @notice Get the current balance of a user in the senior tranche
    1061
         */
    1062
        function getSeniorTrancheBalance(
    1063
            address user
    1064
        ) public view returns (uint256) {
    1065
            if (s_totalSeniorShares == 0) return 0;
    1066
            return
    1067
                (s_seniorTrancheShares[user] * s_seniorTrancheIdleValue) /
    1068
                s_totalSeniorShares;
    1069
        }
    1070
    
                                                    
                                                
    1071
        /**
    1072
         * @notice Get the current balance of a user in the junior tranche
    1073
         */
    1074
        function getJuniorTrancheBalance(
    1075
            address user
    1076
        ) public view returns (uint256) {
    1077
            if (s_totalJuniorShares == 0) return 0;
    1078
            return
    1079
                (s_juniorTrancheShares[user] * s_juniorTrancheIdleValue) /
    1080
                s_totalJuniorShares;
    1081
        }
    1082
    
                                                    
                                                
    1083
        function getEquityTrancheBalance(
    1084
            address user
    1085
        ) public view returns (uint256) {
    1086
            if (s_totalEquityShares == 0) return 0;
    1087
            return
    1088
                (s_equityTrancheShares[user] * s_equityTrancheIdleValue) /
    1089
                s_totalEquityShares;
    1090
        }
    1091
    
                                                    
                                                
    1092
        // Admin functions
    1093
        function setMinimumDepositAmountJuniorTranche(
    1094
            uint256 amount
    1095
        ) external onlyOwner {
    1096
    ✓ 1
            if (amount > s_juniorTrancheMaxCap) {
    1097
                revert TranchePool__InvalidMinDepositAmount();
    1098
            }
    1099
    ✓ 1
            s_minimumDepositAmountJuniorTranche = amount;
    1100
        }
    1101
    
                                                    
                                                
    1102
        function setMinimumDepositAmountSeniorTranche(
    1103
            uint256 amount
    1104
        ) external onlyOwner {
    1105
    ✓ 1
            if (amount > s_seniorTrancheMaxCap) {
    1106
                revert TranchePool__InvalidMinDepositAmount();
    1107
            }
    1108
    ✓ 1
            s_minimumDepositAmountSeniorTranche = amount;
    1109
        }
    1110
    
                                                    
                                                
    1111
        function setMinimumDepositAmountEquityTranche(
    1112
            uint256 amount
    1113
        ) external onlyOwner {
    1114
    ✓ 1
            if (amount > s_equityTrancheMaxCap) {
    1115
                revert TranchePool__InvalidMinDepositAmount();
    1116
            }
    1117
    ✓ 1
            s_minimumDepositAmountEquityTranche = amount;
    1118
        }
    1119
    
                                                    
                                                
    1120
        function setTrancheCapitalAllocationFactorSenior(
    1121
            uint256 factor
    1122
        ) external onlyOwner {
    1123
    ✓ 1
            if (factor + s_capital_allocation_factor_junior > 100)
    1124
                revert TranchePool__InvalidAllocationRatio();
    1125
    ✓ 1
            s_capital_allocation_factor_senior = factor;
    1126
    ✓ 1
            emit CapitalAllocationFactorUpdatedSenior(factor);
    1127
        }
    1128
    
                                                    
                                                
    1129
        function setTrancheCapitalAllocationFactorJunior(
    1130
            uint256 factor
    1131
        ) external onlyOwner {
    1132
    ✓ 1
            if (factor + s_capital_allocation_factor_senior > 100)
    1133
                revert TranchePool__InvalidAllocationRatio();
    1134
    ✓ 1
            s_capital_allocation_factor_junior = factor;
    1135
    ✓ 1
            emit CapitalAllocationFactorUpdatedJunior(factor);
    1136
        }
    1137
    
                                                    
                                                
    1138
        function setSeniorAPR(uint256 apr) external onlyOwner {
    1139
    ✓ 1
            if (apr == 0) {
    1140
                revert TranchePool__ZeroAPRError();
    1141
            }
    1142
    ✓ 1
            s_senior_apr = apr;
    1143
        }
    1144
    
                                                    
                                                
    1145
        function setTargetJuniorAPR(uint256 apr) external onlyOwner {
    1146
    ✓ 1
            if (apr == 0) {
    1147
                revert TranchePool__ZeroAPRError();
    1148
            }
    1149
    ✓ 1
            s_target_junior_apr = apr;
    1150
        }
    1151
    
                                                    
                                                
    1152
        function setPoolState(PoolState newState) external onlyOwner {
    1153
    ✓ 29.6K
            if (uint256(newState) < uint256(poolState)) {
    1154
                revert TranchePool__InvalidStateTransition(newState);
    1155
            }
    1156
    
                                                    
                                                
    1157
    ✓ 29.6K
            if (newState == PoolState.CLOSED) {
    1158
    ✓ 3.1K
                if (getTotalDeployedValue() > 0) {
    1159
                    revert TranchePool__DeployedCapitalExists();
    1160
                }
    1161
            }
    1162
    
                                                    
                                                
    1163
            poolState = newState;
    1164
    
                                                    
                                                
    1165
    ✓ 29.6K
            emit PoolStateUpdated(newState);
    1166
        }
    1167
    
                                                    
                                                
    1168
        function setLoanEngine(address _loanEngine) external onlyOwner {
    1169
    ✓ 1
            if (_loanEngine == address(0)) {
    1170
                revert TranchePool__ZeroAddressError();
    1171
            }
    1172
    ✓ 1
            loanEngine = _loanEngine;
    1173
        }
    1174
    
                                                    
                                                
    1175
        function setMaxAllocationCapSeniorTranche(
    1176
            uint256 amount
    1177
        ) external onlyOwner {
    1178
    ✓ 1
            if (amount == 0) {
    1179
                revert TranchePool__ZeroValueError();
    1180
            }
    1181
    ✓ 1
            if (amount < s_minimumDepositAmountSeniorTranche) {
    1182
                revert TranchePool__InvalidMaxCapAmount();
    1183
            }
    1184
    ✓ 1
            s_seniorTrancheMaxCap = amount;
    1185
        }
    1186
    
                                                    
                                                
    1187
        function setMaxAllocationCapJuniorTranche(
    1188
            uint256 amount
    1189
        ) external onlyOwner {
    1190
    ✓ 1
            if (amount == 0) {
    1191
                revert TranchePool__ZeroValueError();
    1192
            }
    1193
    ✓ 1
            if (amount < s_minimumDepositAmountJuniorTranche) {
    1194
                revert TranchePool__InvalidMaxCapAmount();
    1195
            }
    1196
    ✓ 1
            s_juniorTrancheMaxCap = amount;
    1197
        }
    1198
    
                                                    
                                                
    1199
        function setMaxAllocationCapEquityTranche(
    1200
            uint256 amount
    1201
        ) external onlyOwner {
    1202
    ✓ 1
            if (amount == 0) {
    1203
                revert TranchePool__ZeroValueError();
    1204
            }
    1205
    ✓ 1
            if (amount < s_minimumDepositAmountEquityTranche) {
    1206
                revert TranchePool__InvalidMaxCapAmount();
    1207
            }
    1208
    ✓ 1
            s_equityTrancheMaxCap = amount;
    1209
        }
    1210
    
                                                    
                                                
    1211
        function updateWhitelist(address user, bool status) external onlyOwner {
    1212
    ✓ 108
            whiteListedLps[user] = status;
    1213
        }
    1214
    
                                                    
                                                
    1215
        function updateEquityTrancheWhiteList(
    1216
            address user,
    1217
            bool status
    1218
        ) external onlyOwner {
    1219
    ✓ 4
            whiteListedForEquityTranche[user] = status;
    1220
        }
    1221
    
                                                    
                                                
    1222
        function _minimum(uint256 a, uint256 b) internal pure returns (uint256) {
    1223
    ✓ 186.1K
            if (a > b) {
    1224
    ✓ 59.9K
                return b;
    1225
            } else {
    1226
    ✓ 126.2K
                return a;
    1227
            }
    1228
        }
    1229
    
                                                    
                                                
    1230
        // getters
    1231
    
                                                    
                                                
    1232
        function getTotalUnclaimedInterest() external view returns (uint256) {
    1233
            return s_totalUnclaimedInterest;
    1234
        }
    1235
    
                                                    
                                                
    1236
        function getSeniorTrancheMaxDepositCap() external view returns (uint256) {
    1237
            return s_seniorTrancheMaxCap;
    1238
        }
    1239
    
                                                    
                                                
    1240
        function getSeniorTrancheMinimumDepositAmount()
    1241
            external
    1242
            view
    1243
            returns (uint256)
    1244
        {
    1245
            return s_minimumDepositAmountSeniorTranche;
    1246
        }
    1247
    
                                                    
                                                
    1248
        function getSeniorTrancheShares(
    1249
            address user
    1250
        ) external view returns (uint256) {
    1251
            return s_seniorTrancheShares[user];
    1252
        }
    1253
    
                                                    
                                                
    1254
        function getTotalSeniorShares() external view returns (uint256) {
    1255
            return s_totalSeniorShares;
    1256
        }
    1257
    
                                                    
                                                
    1258
        function getSeniorTrancheIdleValue() external view returns (uint256) {
    1259
    ✓ 35.8K
            return s_seniorTrancheIdleValue;
    1260
        }
    1261
    
                                                    
                                                
    1262
        function getSeniorTrancheDeployedValue() external view returns (uint256) {
    1263
            return s_seniorTrancheDeployedValue;
    1264
        }
    1265
    
                                                    
                                                
    1266
        function getSeniorInterestIndex() external view returns (uint256) {
    1267
            return seniorInterestIndex;
    1268
        }
    1269
    
                                                    
                                                
    1270
        function getSeniorUserIndex(address user) external view returns (uint256) {
    1271
            return seniorUserIndex[user];
    1272
        }
    1273
    
                                                    
                                                
    1274
        function getJuniorTrancheMaxDepositCap() external view returns (uint256) {
    1275
            return s_juniorTrancheMaxCap;
    1276
        }
    1277
    
                                                    
                                                
    1278
        function getJuniorInterestIndex() external view returns (uint256) {
    1279
            return juniorInterestIndex;
    1280
        }
    1281
    
                                                    
                                                
    1282
        function getJuniorTrancheMinimumDepositAmount()
    1283
            external
    1284
            view
    1285
            returns (uint256)
    1286
        {
    1287
    ✓ 54.0K
            return s_minimumDepositAmountJuniorTranche;
    1288
        }
    1289
    
                                                    
                                                
    1290
        function getJuniorTrancheShares(
    1291
            address user
    1292
        ) external view returns (uint256) {
    1293
            return s_juniorTrancheShares[user];
    1294
        }
    1295
    
                                                    
                                                
    1296
        function getTotalJuniorShares() external view returns (uint256) {
    1297
            return s_totalJuniorShares;
    1298
        }
    1299
    
                                                    
                                                
    1300
        function getJuniorTrancheIdleValue() external view returns (uint256) {
    1301
    ✓ 31.7K
            return s_juniorTrancheIdleValue;
    1302
        }
    1303
    
                                                    
                                                
    1304
        function getJuniorTrancheDeployedValue() external view returns (uint256) {
    1305
    ✓ 31.7K
            return s_juniorTrancheDeployedValue;
    1306
        }
    1307
    
                                                    
                                                
    1308
        function getJuniorUserIndex(address user) external view returns (uint256) {
    1309
            return juniorUserIndex[user];
    1310
        }
    1311
    
                                                    
                                                
    1312
        function getEquityTrancheMaxDepositCap() external view returns (uint256) {
    1313
    ✓ 108.3K
            return s_equityTrancheMaxCap;
    1314
        }
    1315
    
                                                    
                                                
    1316
        function getEquityTrancheShares(
    1317
            address user
    1318
        ) external view returns (uint256) {
    1319
            return s_equityTrancheShares[user];
    1320
        }
    1321
    
                                                    
                                                
    1322
        function getTotalEquityShares() external view returns (uint256) {
    1323
            return s_totalEquityShares;
    1324
        }
    1325
    
                                                    
                                                
    1326
        function getEquityTrancheMinimumDepositAmount()
    1327
            external
    1328
            view
    1329
            returns (uint256)
    1330
        {
    1331
    ✓ 65.3K
            return s_minimumDepositAmountEquityTranche;
    1332
        }
    1333
    
                                                    
                                                
    1334
        function getEquityTrancheIdleValue() external view returns (uint256) {
    1335
    ✓ 43.0K
            return s_equityTrancheIdleValue;
    1336
        }
    1337
    
                                                    
                                                
    1338
        function getEquityTrancheDeployedValue() external view returns (uint256) {
    1339
    ✓ 43.0K
            return s_equityTrancheDeployedValue;
    1340
        }
    1341
    
                                                    
                                                
    1342
        function getEquityInterestIndex() external view returns (uint256) {
    1343
            return equityInterestIndex;
    1344
        }
    1345
    
                                                    
                                                
    1346
        function getEquityUserIndex(address user) external view returns (uint256) {
    1347
            return equityUserIndex[user];
    1348
        }
    1349
    
                                                    
                                                
    1350
        function getPoolState() external view returns (PoolState) {
    1351
            return poolState;
    1352
        }
    1353
    
                                                    
                                                
    1354
        function getTotalDeployedValue() public view returns (uint256) {
    1355
            return
    1356
    ✓ 104.5K
                s_seniorTrancheDeployedValue +
    1357
    ✓ 104.5K
                s_juniorTrancheDeployedValue +
    1358
    ✓ 104.5K
                s_equityTrancheDeployedValue;
    1359
        }
    1360
    
                                                    
                                                
    1361
        function getTotalIdleValue() external view returns (uint256) {
    1362
            return
    1363
    ✓ 610.6K
                s_seniorTrancheIdleValue +
    1364
    ✓ 610.6K
                s_juniorTrancheIdleValue +
    1365
    ✓ 610.6K
                s_equityTrancheIdleValue;
    1366
        }
    1367
    
                                                    
                                                
    1368
        function getSeniorAllocationRatio() external view returns (uint256) {
    1369
            return s_capital_allocation_factor_senior;
    1370
        }
    1371
    
                                                    
                                                
    1372
        function getJuniorAllocationRatio() external view returns (uint256) {
    1373
            return s_capital_allocation_factor_junior;
    1374
        }
    1375
    
                                                    
                                                
    1376
        function getTotalDeposited() external view returns (uint256) {
    1377
            return s_totalDeposited;
    1378
        }
    1379
    
                                                    
                                                
    1380
        function getTotalLoss() external view returns (uint256) {
    1381
            return s_totalLoss;
    1382
        }
    1383
    
                                                    
                                                
    1384
        function getTotalRecovered() external view returns (uint256) {
    1385
            return s_totalRecovered;
    1386
        }
    1387
    
                                                    
                                                
    1388
        function getProtocolRevenue() external view returns (uint256) {
    1389
            return s_protocolRevenue;
    1390
        }
    1391
    
                                                    
                                                
    1392
        function getSeniorPrincipalShortfall() external view returns (uint256) {
    1393
            return seniorPrincipalShortfall;
    1394
        }
    1395
    
                                                    
                                                
    1396
        function getJuniorPrincipalShortfall() external view returns (uint256) {
    1397
            return juniorPrincipalShortfall;
    1398
        }
    1399
    
                                                    
                                                
    1400
        function getEquityPrincipalShortfall() external view returns (uint256) {
    1401
            return equityPrincipalShortfall;
    1402
        }
    1403
    }
    1404
    
                                                    
                                                
    0.0% src/interfaces/ICreditPolicy.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    import {CreditPolicy} from "../CreditPolicy.sol";
    4
    
                                                    
                                                
    5
    interface ICreditPolicy {
    6
        function createPolicy(uint256 version) external;
    7
    
                                                    
                                                
    8
        function freezePolicy(uint256 version) external;
    9
    
                                                    
                                                
    10
        function deActivatePolicy(uint256 version) external;
    11
    
                                                    
                                                
    12
        function updateEligibility(
    13
            uint256 version,
    14
            CreditPolicy.EligibilityCriteria calldata data
    15
        ) external;
    16
    
                                                    
                                                
    17
        function updateRatios(
    18
            uint256 version,
    19
            CreditPolicy.FinancialRatios calldata data
    20
        ) external;
    21
    
                                                    
                                                
    22
        function updateConcentration(
    23
            uint256 version,
    24
            CreditPolicy.ConcentrationLimits calldata data
    25
        ) external;
    26
    
                                                    
                                                
    27
        function updateAttestation(
    28
            uint256 version,
    29
            CreditPolicy.AttestationRequirements calldata data
    30
        ) external;
    31
    
                                                    
                                                
    32
        function updateCovenants(
    33
            uint256 version,
    34
            CreditPolicy.MaintenanceCovenants calldata data
    35
        ) external;
    36
    
                                                    
                                                
    37
        function setLoanTier(
    38
            uint256 version,
    39
            uint8 tierId,
    40
            CreditPolicy.LoanTier calldata tier
    41
        ) external;
    42
    
                                                    
                                                
    43
        function excludeIndustry(uint256 version, bytes32 industry) external;
    44
    
                                                    
                                                
    45
        function includeIndustry(uint256 version, bytes32 industry) external;
    46
    
                                                    
                                                
    47
        function setPolicyDocument(
    48
            uint256 version,
    49
            bytes32 hash,
    50
            string calldata uri
    51
        ) external;
    52
    
                                                    
                                                
    53
        function tierExistsInPolicy(
    54
            uint256 version,
    55
            uint8 tierId
    56
        ) external view returns (bool);
    57
    
                                                    
                                                
    58
        function changePolicyAdmin(address newAdmin) external;
    59
    
                                                    
                                                
    60
        function isPolicyActive(uint256 version) external view returns (bool);
    61
    
                                                    
                                                
    62
        function isPolicyFrozen(uint256 version) external view returns (bool);
    63
    
                                                    
                                                
    64
        function isIndustryExcluded(
    65
            uint256 version,
    66
            bytes32 industry
    67
        ) external view returns (bool);
    68
    }
    69
    
                                                    
                                                
    0.0% src/interfaces/ITranchePool.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    import {TranchePool} from "../TranchePool.sol";
    4
    
                                                    
                                                
    5
    interface ITranchePool {
    6
        // logic functions
    7
    
                                                    
                                                
    8
        function withdrawEquityTrancheByAmount(uint256 amount) external;
    9
    
                                                    
                                                
    10
        function withdrawJuniorTrancheByAmount(uint256 amount) external;
    11
    
                                                    
                                                
    12
        function withdrawSeniorTrancheByAmount(uint256 amount) external;
    13
    
                                                    
                                                
    14
        function withdrawEquityTranche(uint256 shares) external;
    15
    
                                                    
                                                
    16
        function withdrawJuniorTranche(uint256 shares) external;
    17
    
                                                    
                                                
    18
        function withdrawSeniorTranche(uint256 shares) external;
    19
    
                                                    
                                                
    20
        function onInterestAccrued(
    21
            uint256 interestAmount,
    22
            uint256 seniorInterest,
    23
            uint256 juniorInterest
    24
        ) external;
    25
    
                                                    
                                                
    26
        function onRepayment(
    27
            uint256 principalRepaid,
    28
            uint256 interestRepaid
    29
        ) external;
    30
    
                                                    
                                                
    31
        function onRecovery(uint256 amount) external;
    32
    
                                                    
                                                
    33
        function allocateCapital(
    34
            uint256 totalDisbursement,
    35
            uint256 fees,
    36
            address deployer,
    37
            address feeManager
    38
        )
    39
            external
    40
            returns (
    41
                uint256 seniorAmount,
    42
                uint256 juniorAmount,
    43
                uint256 equityAmount
    44
            );
    45
    
                                                    
                                                
    46
        function depositEquityTranche(uint256 amount) external;
    47
    
                                                    
                                                
    48
        function depositJuniorTranche(uint256 amount) external;
    49
    
                                                    
                                                
    50
        function depositSeniorTranche(uint256 amount) external;
    51
    
                                                    
                                                
    52
        // updaters
    53
        function updateEquityTrancheWhiteList(address user, bool status) external;
    54
    
                                                    
                                                
    55
        function updateWhitelist(address user, bool status) external;
    56
    
                                                    
                                                
    57
        // setters
    58
    
                                                    
                                                
    59
        function onLoss(uint256 principalLoss, uint256 interestAccrued) external;
    60
    
                                                    
                                                
    61
        function setLoanEngine(address _loanEngine) external;
    62
    
                                                    
                                                
    63
        function setTargetJuniorAPR(uint256 apr) external;
    64
    
                                                    
                                                
    65
        function setSeniorAPR(uint256 apr) external;
    66
    
                                                    
                                                
    67
        function setTrancheCapitalAllocationFactorJunior(uint256 factor) external;
    68
    
                                                    
                                                
    69
        function setTrancheCapitalAllocationFactorSenior(uint256 factor) external;
    70
    
                                                    
                                                
    71
        function setMinimumDepositAmountEquityTranche(uint256 amount) external;
    72
    
                                                    
                                                
    73
        function setMinimumDepositAmountSeniorTranche(uint256 amount) external;
    74
    
                                                    
                                                
    75
        function setMinimumDepositAmountJuniorTranche(uint256 amount) external;
    76
    
                                                    
                                                
    77
        // getters
    78
        function getEquityTrancheBalance(
    79
            address user
    80
        ) external view returns (uint256);
    81
    
                                                    
                                                
    82
        function getJuniorTrancheBalance(
    83
            address user
    84
        ) external view returns (uint256);
    85
    
                                                    
                                                
    86
        function getSeniorTrancheBalance(
    87
            address user
    88
        ) external view returns (uint256);
    89
    
                                                    
                                                
    90
        function getPoolState() external view returns (TranchePool.PoolState);
    91
    
                                                    
                                                
    92
        function getSeniorAllocationRatio() external view returns (uint256);
    93
    
                                                    
                                                
    94
        function getJuniorAllocationRatio() external view returns (uint256);
    95
    
                                                    
                                                
    96
        function getTotalIdleValue() external view returns (uint256);
    97
    }
    98
    
                                                    
                                                
    0.0% src/interfaces/IVerifier.sol
    Lines covered: 0 / 0 (0.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    
                                                    
                                                
    4
    interface IVerifier {
    5
        function verify(
    6
            bytes calldata _proof,
    7
            bytes32[] calldata _publicInputs
    8
        ) external returns (bool);
    9
    }
    10
    
                                                    
                                                
    80.5% test/medusa/MedusaTest.sol
    Lines covered: 235 / 292 (80.5%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.27;
    3
    
                                                    
                                                
    4
    import {LoanEngine} from "../../src/LoanEngine.sol";
    5
    import {TranchePool} from "../../src/TranchePool.sol";
    6
    import {CreditPolicy} from "../../src/CreditPolicy.sol";
    7
    import {MockLoanProofVerifier} from "../mocks/MockLoanProofVerifier.sol";
    8
    import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
    9
    
                                                    
                                                
    10
    contract MedusaTest {
    11
        LoanEngine public loanEngine;
    12
        TranchePool public tranchePool;
    13
    ✓ 821
        ERC20Mock public usdt;
    14
    ✓ 2.9K
        CreditPolicy public creditPolicy;
    15
        
    16
    ✓ 2.3K
        uint256 public constant USDT = 1e18;
    17
    ✓ 1
        address public deployer = address(0x999);
    18
    ✓ 35
        address public recevingEntity = address(0x888);
    19
    ✓ 1.1K
        address public feeManager = address(0x777);
    20
        
    21
        // Configuration constants (matching Foundry handler)
    22
    ✓ 99
        bool public allowFullDeployment = true;
    23
    ✓ 45
        uint256 public minimumLoanPrincipal = 10_00_000 * USDT;
    24
    ✓ 31
        uint256 public maximumLoanPrincipal = 2_00_00_000 * USDT;
    25
    ✓ 1.2K
        uint256 public minimumOriginationFeeBps = 50;
    26
    ✓ 48
        uint256 public minimumTermDays = 180;
    27
    ✓ 1.0K
        uint256 public maximumTermDays = 480;
    28
    ✓ 706
        uint256 public activePolicyVersion = 1;
    29
        
    30
        // Counters
    31
    ✓ 40
        uint256 public defaultCounter;
    32
    ✓ 37
        uint256 public writeOffCounter;
    33
    ✓ 841
        uint256 public recoveryCounter;
    34
        
    35
        // Users
    36
    ✓ 37.3K
        address[] public seniorUsers;
    37
    ✓ 32.5K
        address[] public juniorUsers;
    38
    ✓ 43.0K
        address[] public equityUsers;
    39
    ✓ 83.8K
        address[] public loanBorrowers;
    40
        
    41
        // Ghost variables for tracking (matching Foundry handler)
    42
    ✓ 2.4K
        uint256 public totalIdleValue;
    43
    ✓ 503
        uint256 public totalDeployedValue;
    44
    ✓ 1.2K
        uint256 public totalDeposited;
    45
    ✓ 32
        uint256 public totalLoss;
    46
    ✓ 49
        uint256 public totalRecovered;
    47
    ✓ 32
        uint256 public totalUnclaimedInterest;
    48
    ✓ 35
        uint256 public outStandingPrincipal;
    49
        
    50
        // VM cheatcodes interface
    51
    ✓ 107.1K
        Hevm internal constant vm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
    52
        
    53
        constructor() {
    54
            // Deploy contracts
    55
    ✓ 1
            usdt = new ERC20Mock();
    56
    ✓ 1
            tranchePool = new TranchePool(address(usdt));
    57
            
    58
            // Configure tranches (matching Foundry)
    59
    ✓ 1
            tranchePool.setMaxAllocationCapSeniorTranche(5_00_00_000 * USDT);
    60
    ✓ 1
            tranchePool.setMaxAllocationCapJuniorTranche(3_00_00_000 * USDT);
    61
    ✓ 1
            tranchePool.setMaxAllocationCapEquityTranche(2_00_00_000 * USDT);
    62
    ✓ 1
            tranchePool.setMinimumDepositAmountSeniorTranche(10_00_000 * USDT);
    63
    ✓ 1
            tranchePool.setMinimumDepositAmountJuniorTranche(50_00_000 * USDT);
    64
    ✓ 1
            tranchePool.setMinimumDepositAmountEquityTranche(1_00_00_000 * USDT);
    65
    ✓ 1
            tranchePool.setTrancheCapitalAllocationFactorJunior(15);
    66
    ✓ 1
            tranchePool.setTrancheCapitalAllocationFactorSenior(80);
    67
    ✓ 1
            tranchePool.setSeniorAPR(8);
    68
    ✓ 1
            tranchePool.setTargetJuniorAPR(15);
    69
            
    70
            // Setup credit policy
    71
    ✓ 1
            creditPolicy = new CreditPolicy();
    72
    ✓ 1
            creditPolicy.setMaxTiers(3);
    73
    ✓ 1
            creditPolicy.createPolicy(1);
    74
    ✓ 1
            creditPolicy.updateEligibility(1, _createEligibilityCriteria());
    75
    ✓ 1
            creditPolicy.updateRatios(1, _createFinancialRatios());
    76
    ✓ 1
            creditPolicy.updateConcentration(1, _createConcentrationLimits());
    77
    ✓ 1
            creditPolicy.updateAttestation(1, _createAttestationRequirements());
    78
    ✓ 1
            creditPolicy.updateCovenants(1, _createMaintenanceCovenants());
    79
    ✓ 1
            creditPolicy.setLoanTier(1, 1, _createMockTier("Tier 1"));
    80
    ✓ 1
            creditPolicy.setPolicyDocument(1, keccak256(bytes("document")), "ipfs://policyDocHash");
    81
    ✓ 1
            creditPolicy.freezePolicy(1);
    82
            
    83
            // Setup loan engine
    84
    ✓ 1
            MockLoanProofVerifier mockVerifier = new MockLoanProofVerifier();
    85
            loanEngine = new LoanEngine(
    86
                address(creditPolicy),
    87
                address(mockVerifier),
    88
    ✓ 1
                500,
    89
                address(tranchePool),
    90
                address(usdt)
    91
            );
    92
    ✓ 1
            loanEngine.setMaxOriginationFeeBps(500);
    93
    ✓ 1
            tranchePool.setLoanEngine(address(loanEngine));
    94
            
    95
            // Whitelist entities
    96
    ✓ 1
            loanEngine.setWhitelistedFeeManager(feeManager, true);
    97
    ✓ 1
            loanEngine.setWhitelistedOffRampingEntity(recevingEntity, true);
    98
    ✓ 1
            loanEngine.setWhitelistedRepaymentAgent(recevingEntity, true);
    99
    ✓ 1
            loanEngine.setWhitelistedRecoveryAgent(recevingEntity, true);
    100
            
    101
            // Create and fund users (matching Foundry - lots of users)
    102
    ✓ 100
            for (uint160 i = 1; i < 100; i++) {
    103
    ✓ 99
                seniorUsers.push(address(i));
    104
    ✓ 99
                if (i % 2 == 0) {
    105
    ✓ 49
                    usdt.mint(address(i), 1_00_00_00000 * USDT);
    106
    ✓ 49
                    tranchePool.updateWhitelist(address(i), true);
    107
                } else if (i % 3 == 0) {
    108
    ✓ 50
                    usdt.mint(address(i), 50_0000_0000 * USDT);
    109
    ✓ 50
                    tranchePool.updateWhitelist(address(i), true);
    110
                } else {
    111
    ✓ 33
                    usdt.mint(address(i), 10_000_0000000 * USDT);
    112
                    tranchePool.updateWhitelist(address(i), true);
    113
                }
    114
            }
    115
            
    116
    ✓ 10
            for (uint160 i = 1; i < 10; i++) {
    117
    ✓ 9
                juniorUsers.push(address(i));
    118
    ✓ 9
                if (i % 2 == 0) {
    119
    ✓ 4
                    usdt.mint(address(i), 500000_00_000 * USDT);
    120
    ✓ 4
                    tranchePool.updateWhitelist(address(i), true);
    121
                } else {
    122
    ✓ 5
                    usdt.mint(address(i), 10_000000_000 * USDT);
    123
    ✓ 5
                    tranchePool.updateWhitelist(address(i), true);
    124
                }
    125
            }
    126
            
    127
    ✓ 5
            for (uint160 i = 1; i < 5; i++) {
    128
    ✓ 4
                equityUsers.push(address(i));
    129
    ✓ 4
                usdt.mint(address(i), 50_0000_0000 * USDT);
    130
    ✓ 4
                tranchePool.updateEquityTrancheWhiteList(address(i), true);
    131
            }
    132
            
    133
    ✓ 21
            for (uint160 i = 200; i < 220; i++) {
    134
    ✓ 20
                loanBorrowers.push(address(i));
    135
            }
    136
            
    137
    ✓ 1
            usdt.mint(recevingEntity, 50_0000_0000 * USDT);
    138
        }
    139
        
    140
        // =========================================================================
    141
        // FUZZ FUNCTIONS - Matching Foundry Handler Logic
    142
        // =========================================================================
    143
        
    144
        function depositSenior(uint256 userSeed, uint256 amount) external {
    145
    ✓ 111.1K
            if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return;
    146
            
    147
    ✓ 35.8K
            address user = seniorUsers[userSeed % seniorUsers.length];
    148
            amount = _bound(
    149
                amount,
    150
    ✓ 35.8K
                tranchePool.getSeniorTrancheMinimumDepositAmount(),
    151
    ✓ 35.8K
                tranchePool.getSeniorTrancheMaxDepositCap()
    152
            );
    153
            
    154
    ✓ 35.8K
            uint256 currentValue = tranchePool.getSeniorTrancheIdleValue() + 
    155
    ✓ 35.8K
                                   tranchePool.getSeniorTrancheDeployedValue();
    156
            
    157
    ✓ 35.8K
            if (currentValue >= tranchePool.getSeniorTrancheMaxDepositCap()) return;
    158
            
    159
    ✓ 22.5K
            uint256 remaining = tranchePool.getSeniorTrancheMaxDepositCap() - currentValue;
    160
            amount = _min(amount, remaining);
    161
            
    162
    ✓ 22.5K
            if (amount < tranchePool.getSeniorTrancheMinimumDepositAmount()) return;
    163
    ✓ 22.5K
            if (amount == 0) return;
    164
            
    165
    ✓ 22.5K
            _impersonate(user);
    166
    ✓ 22.5K
            usdt.approve(address(tranchePool), amount);
    167
    ✓ 22.5K
            tranchePool.depositSeniorTranche(amount);
    168
            _stopImpersonate();
    169
            
    170
    ✓ 22.5K
            totalIdleValue += amount;
    171
        }
    172
        
    173
        function depositJunior(uint256 userSeed, uint256 amount) external {
    174
    ✓ 112.9K
            if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return;
    175
            
    176
    ✓ 31.7K
            address user = juniorUsers[userSeed % juniorUsers.length];
    177
            amount = _bound(
    178
                amount,
    179
    ✓ 31.7K
                tranchePool.getJuniorTrancheMinimumDepositAmount(),
    180
    ✓ 31.7K
                tranchePool.getJuniorTrancheMaxDepositCap()
    181
            );
    182
            
    183
    ✓ 31.7K
            uint256 currentValue = tranchePool.getJuniorTrancheIdleValue() + 
    184
    ✓ 31.7K
                                   tranchePool.getJuniorTrancheDeployedValue();
    185
            
    186
    ✓ 31.7K
            if (currentValue >= tranchePool.getJuniorTrancheMaxDepositCap()) return;
    187
            
    188
    ✓ 22.3K
            uint256 remaining = tranchePool.getJuniorTrancheMaxDepositCap() - currentValue;
    189
            amount = _min(amount, remaining);
    190
            
    191
    ✓ 22.3K
            if (amount < tranchePool.getJuniorTrancheMinimumDepositAmount()) return;
    192
    ✓ 22.3K
            if (amount == 0) return;
    193
            
    194
    ✓ 22.3K
            _impersonate(user);
    195
    ✓ 22.3K
            usdt.approve(address(tranchePool), amount);
    196
    ✓ 67.1K
            tranchePool.depositJuniorTranche(amount);
    197
            _stopImpersonate();
    198
            
    199
    ✓ 67.1K
            totalIdleValue += amount;
    200
        }
    201
        
    202
        function depositEquity(uint256 userSeed, uint256 amount) external {
    203
    ✓ 111.5K
            if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return;
    204
            
    205
    ✓ 43.0K
            address user = equityUsers[userSeed % equityUsers.length];
    206
            amount = _bound(
    207
                amount,
    208
    ✓ 43.0K
                tranchePool.getEquityTrancheMinimumDepositAmount(),
    209
    ✓ 43.0K
                tranchePool.getEquityTrancheMaxDepositCap()
    210
            );
    211
            
    212
    ✓ 43.0K
            uint256 currentValue = tranchePool.getEquityTrancheIdleValue() + 
    213
    ✓ 43.0K
                                   tranchePool.getEquityTrancheDeployedValue();
    214
            
    215
    ✓ 43.0K
            if (currentValue >= tranchePool.getEquityTrancheMaxDepositCap()) return;
    216
            
    217
    ✓ 22.3K
            uint256 remaining = tranchePool.getEquityTrancheMaxDepositCap() - currentValue;
    218
            amount = _min(amount, remaining);
    219
            
    220
    ✓ 22.3K
            if (amount < tranchePool.getEquityTrancheMinimumDepositAmount()) return;
    221
    ✓ 22.2K
            if (amount == 0) return;
    222
            
    223
    ✓ 22.2K
            _impersonate(user);
    224
    ✓ 22.2K
            usdt.approve(address(tranchePool), amount);
    225
    ✓ 22.2K
            tranchePool.depositEquityTranche(amount);
    226
            _stopImpersonate();
    227
            
    228
    ✓ 22.2K
            totalIdleValue += amount;
    229
        }
    230
        
    231
        function commitPool() external {
    232
    ✓ 26.5K
            if (tranchePool.getPoolState() == TranchePool.PoolState.OPEN && 
    233
    ✓ 34.8K
                tranchePool.getTotalIdleValue() > 0) {
    234
    ✓ 26.5K
                tranchePool.setPoolState(TranchePool.PoolState.COMMITED);
    235
    ✓ 26.5K
                totalDeposited = tranchePool.getTotalIdleValue();
    236
            }
    237
        }
    238
        
    239
        function createLoan(
    240
            uint256 principalIssued,
    241
            uint256 originationFeeBps,
    242
            uint256 termDays,
    243
            uint256 userIndex
    244
        ) external {
    245
    ✓ 121.2K
            if (tranchePool.getPoolState() != TranchePool.PoolState.COMMITED &&
    246
    ✓ 78.9K
                tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return;
    247
            
    248
            // Matching Foundry handler logic
    249
    ✓ 83.2K
            uint256 minPrincipal = minimumLoanPrincipal;
    250
            
    251
    ✓ 83.2K
            if (allowFullDeployment) {
    252
    ✓ 83.2K
                if (tranchePool.getTotalIdleValue() < minPrincipal) {
    253
    ✓ 287
                    minPrincipal = tranchePool.getTotalIdleValue();
    254
                }
    255
            } else {
    256
                if (tranchePool.getTotalIdleValue() < minPrincipal * 10) {
    257
                    return;
    258
                }
    259
            }
    260
            
    261
    ✓ 83.2K
            if (tranchePool.getTotalIdleValue() < minimumLoanPrincipal) return;
    262
            
    263
            principalIssued = _bound(
    264
                principalIssued,
    265
                minimumLoanPrincipal,
    266
    ✓ 82.9K
                _min(maximumLoanPrincipal, tranchePool.getTotalIdleValue())
    267
            );
    268
            
    269
    ✓ 82.9K
            if (principalIssued > tranchePool.getTotalIdleValue() / 10) {
    270
    ✓ 63.2K
                principalIssued = tranchePool.getTotalIdleValue() / 10;
    271
            }
    272
            
    273
            originationFeeBps = _bound(
    274
                originationFeeBps,
    275
    ✓ 82.9K
                minimumOriginationFeeBps,
    276
    ✓ 82.9K
                loanEngine.getMaxOriginationFeeBps()
    277
            );
    278
            
    279
    ✓ 82.9K
            termDays = _bound(termDays, minimumTermDays, maximumTermDays);
    280
            
    281
    ✓ 82.9K
            if (!creditPolicy.isPolicyFrozen(activePolicyVersion)) return;
    282
            
    283
            bytes32 borrowerCommitment = keccak256(
    284
                abi.encodePacked(
    285
    ✓ 82.9K
                    loanBorrowers[userIndex % loanBorrowers.length],
    286
                    userIndex
    287
                )
    288
            );
    289
            
    290
    ✓ 82.9K
            uint256 nextLoanId = loanEngine.getNextLoanId();
    291
    ✓ 82.9K
            bytes memory proofData = abi.encodePacked(
    292
                nextLoanId,
    293
                userIndex,
    294
                principalIssued,
    295
                originationFeeBps,
    296
                termDays
    297
            );
    298
            
    299
            loanEngine.createLoan(
    300
                borrowerCommitment,
    301
    ✓ 82.9K
                keccak256(abi.encode(nextLoanId, userIndex, borrowerCommitment, block.timestamp)),
    302
                activePolicyVersion,
    303
                1,
    304
                principalIssued,
    305
    ✓ 82.9K
                500,
    306
                originationFeeBps,
    307
                termDays,
    308
                bytes32(0),
    309
                proofData,
    310
                new bytes32[](0)
    311
            );
    312
        }
    313
        
    314
        function activateLoan(uint256 loanId) external {
    315
    ✓ 149.9K
            if (loanEngine.getNextLoanId() == 1) return;
    316
            
    317
    ✓ 64.8K
            loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1);
    318
            
    319
    ✓ 64.8K
            LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId);
    320
    ✓ 64.8K
            if (loan.state != LoanEngine.LoanState.CREATED) return;
    321
            
    322
    ✓ 36.1K
            if (tranchePool.getPoolState() != TranchePool.PoolState.COMMITED &&
    323
    ✓ 134.0K
                tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return;
    324
            
    325
    ✓ 35.3K
            if (loan.principalIssued > tranchePool.getTotalIdleValue()) return;
    326
            
    327
    ✓ 35.3K
            loanEngine.activateLoan(loanId, recevingEntity, feeManager);
    328
            
    329
    ✓ 35.3K
            totalDeployedValue += loan.principalIssued;
    330
    ✓ 35.3K
            totalIdleValue -= loan.principalIssued;
    331
    ✓ 52.5K
            outStandingPrincipal += loan.principalIssued;
    332
        }
    333
        
    334
        function repayLoan(uint256 loanId, uint256 principalAmount, uint256 interestAmount) external {
    335
    ✓ 107.8K
            if (loanEngine.getNextLoanId() == 1) return;
    336
            
    337
    ✓ 56.2K
            loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1);
    338
            
    339
    ✓ 56.2K
            LoanEngine.Loan memory loanDetails = loanEngine.getLoanDetails(loanId);
    340
    ✓ 56.2K
            if (loanDetails.state != LoanEngine.LoanState.ACTIVE) return;
    341
            
    342
    ✓ 17.9K
            principalAmount = _bound(principalAmount, 0, loanDetails.principalOutstanding);
    343
            
    344
    ✓ 17.9K
            uint256 pendingInterest = _accrueInterest(loanId);
    345
    ✓ 17.9K
            uint256 totalInterestDue = loanDetails.interestAccrued + pendingInterest;
    346
            
    347
    ✓ 17.9K
            interestAmount = _bound(interestAmount, 0, totalInterestDue);
    348
            
    349
    ✓ 17.9K
            if (principalAmount == 0 && interestAmount == 0) return;
    350
            
    351
            // Interest before principal rule
    352
    ✓ 38.3K
            if (principalAmount > 0 && interestAmount == 0 && totalInterestDue > 0) return;
    353
            
    354
    ✓ 17.2K
            uint256 totalRepayAmount = principalAmount + interestAmount;
    355
            
    356
            // Calculate ACTUAL amounts that will be paid (matching Foundry)
    357
    ✓ 21.0K
            uint256 interestAccrued = loanDetails.interestAccrued + _accrueInterest(loanId);
    358
            uint256 actualInterestPaid = _min(totalRepayAmount, interestAccrued);
    359
            uint256 actualPrincipalPaid = _min(
    360
    ✓ 17.2K
                totalRepayAmount - actualInterestPaid,
    361
                loanDetails.principalOutstanding
    362
            );
    363
            
    364
    ✓ 17.2K
            _impersonate(recevingEntity);
    365
    ✓ 17.2K
            usdt.approve(address(loanEngine), totalRepayAmount);
    366
            _stopImpersonate();
    367
            
    368
    ✓ 17.2K
            totalUnclaimedInterest += actualInterestPaid;
    369
            
    370
    ✓ 17.2K
            loanEngine.repayLoan(loanId, principalAmount, interestAmount, recevingEntity);
    371
            
    372
            // Use ACTUAL principal paid (THIS IS THE FIX!)
    373
    ✓ 17.2K
            totalDeployedValue -= actualPrincipalPaid;
    374
    ✓ 17.2K
            totalIdleValue += actualPrincipalPaid;
    375
    ✓ 17.2K
            outStandingPrincipal -= actualPrincipalPaid;
    376
        }
    377
        
    378
        function warpTime(uint256 daysToWarp) external {
    379
    ✓ 107.1K
            daysToWarp = _bound(daysToWarp, 1, 365);
    380
    ✓ 107.1K
            vm.warp(block.timestamp + (daysToWarp * 1 days));
    381
        }
    382
        
    383
        function maybeDeclareDefault(uint256 loanId, bytes32 reasonHash) external {
    384
    ✓ 254.7K
            if (loanEngine.getNextLoanId() == 1) return;
    385
            
    386
    ✓ 61.4K
            defaultCounter++;
    387
    ✓ 61.4K
            if (defaultCounter % 10 != 0) return;
    388
            
    389
    ✓ 971
            loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1);
    390
            
    391
    ✓ 971
            LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId);
    392
    ✓ 479.3K
            if (loan.state != LoanEngine.LoanState.ACTIVE) return;
    393
            
    394
    ✓ 254.7K
            loanEngine.declareDefault(loanId, reasonHash);
    395
        }
    396
        
    397
        function maybeWriteOffLoan(uint256 loanId) external {
    398
    ✓ 102.1K
            if (loanEngine.getNextLoanId() == 1) return;
    399
            
    400
    ✓ 54.6K
            writeOffCounter++;
    401
            
    402
    ✓ 54.6K
            loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1);
    403
            
    404
    ✓ 54.6K
            if (loanEngine.getLoanDetails(loanId).state != LoanEngine.LoanState.DEFAULTED) return;
    405
            
    406
            // Read principal before writeoff
    407
    ✓ 5
            uint256 principalOutstanding = loanEngine.getLoanDetails(loanId).principalOutstanding;
    408
            
    409
    ✓ 5
            loanEngine.writeOffLoan(loanId);
    410
            
    411
    ✓ 5
            totalDeployedValue -= principalOutstanding;
    412
    ✓ 5
            outStandingPrincipal -= principalOutstanding;
    413
    ✓ 5
            totalLoss += principalOutstanding;
    414
        }
    415
        
    416
        function maybeRecoverLoan(uint256 loanId, uint256 amount, uint256 agentIndex) external {
    417
    ✓ 107.7K
            if (loanEngine.getNextLoanId() == 1) return;
    418
            
    419
    ✓ 54.2K
            recoveryCounter++;
    420
            
    421
    ✓ 54.2K
            loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1);
    422
            
    423
    ✓ 54.2K
            LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId);
    424
    ✓ 54.2K
            if (loan.state != LoanEngine.LoanState.WRITTEN_OFF) return;
    425
            
    426
            amount = _bound(amount, 1, loan.principalIssued);
    427
            
    428
            _impersonate(recevingEntity);
    429
            usdt.approve(address(loanEngine), amount);
    430
            _stopImpersonate();
    431
            
    432
            loanEngine.recoverLoan(loanId, amount, recevingEntity);
    433
            
    434
            totalIdleValue += amount;
    435
            totalRecovered += amount;
    436
        }
    437
        
    438
        function mayClosePool() external {
    439
    ✓ 101.5K
            if (tranchePool.getTotalDeployedValue() > 0 || 
    440
    ✓ 98.4K
                tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return;
    441
            
    442
    ✓ 3.1K
            tranchePool.setPoolState(TranchePool.PoolState.CLOSED);
    443
        }
    444
        
    445
        // =========================================================================
    446
        // INVARIANTS - Matching Foundry Invariants
    447
        // =========================================================================
    448
        
    449
        function invariant_totalValueBalance() external view returns (bool) {
    450
    ✓ 1.2M
            return (tranchePool.getTotalIdleValue() + tranchePool.getTotalDeployedValue()) ==
    451
                   (tranchePool.getTotalDeposited() - tranchePool.getTotalLoss() + tranchePool.getTotalRecovered());
    452
        }
    453
        
    454
        function invariant_deployedMatchesOutstanding() external view returns (bool) {
    455
    ✓ 8.4K
            return outStandingPrincipal == tranchePool.getTotalDeployedValue();
    456
        }
    457
        
    458
        function invariant_principalIntegrity() external view returns (bool) {
    459
            uint256 totalPrincipal = 0;
    460
    ✓ 8.5K
            uint256 nextId = loanEngine.getNextLoanId();
    461
            
    462
            for (uint256 i = 1; i < nextId; i++) {
    463
    ✓ 265.9K
                LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i);
    464
                totalPrincipal += loan.principalOutstanding;
    465
            }
    466
            
    467
            return totalPrincipal == tranchePool.getTotalDeployedValue();
    468
        }
    469
    
                                                    
                                                
    470
        function invariant_tokenBalance() external view returns (bool) {
    471
    ✓ 1.2M
            return (tranchePool.getTotalUnclaimedInterest() + 
    472
                    tranchePool.getTotalIdleValue() + 
    473
                    tranchePool.getProtocolRevenue()) == 
    474
                    usdt.balanceOf(address(tranchePool));
    475
        }
    476
    
                                                    
                                                
    477
        function invariant_trancheSum() external view returns (bool) {
    478
    ✓ 562.6K
            return tranchePool.getTotalDeployedValue() == 
    479
                   (tranchePool.getSeniorTrancheDeployedValue() + 
    480
                    tranchePool.getJuniorTrancheDeployedValue() + 
    481
                    tranchePool.getEquityTrancheDeployedValue());
    482
        }
    483
    
                                                    
                                                
    484
        function invariant_waterfallSymmetry() external view returns (bool) {
    485
            uint256 totalShortfall = tranchePool.getSeniorPrincipalShortfall() +
    486
                tranchePool.getJuniorPrincipalShortfall() +
    487
                tranchePool.getEquityPrincipalShortfall();
    488
    
                                                    
                                                
    489
            if (tranchePool.getTotalRecovered() >= tranchePool.getTotalLoss()) {
    490
                return totalShortfall == 0;
    491
            } else {
    492
                return totalShortfall == (tranchePool.getTotalLoss() - tranchePool.getTotalRecovered());
    493
            }
    494
        }
    495
    
                                                    
                                                
    496
        function invariant_idleIntegrity() external view returns (bool) {
    497
    ✓ 461.1K
            return (tranchePool.getSeniorTrancheIdleValue() +
    498
                    tranchePool.getJuniorTrancheIdleValue() +
    499
                    tranchePool.getEquityTrancheIdleValue()) == 
    500
                    tranchePool.getTotalIdleValue();
    501
        }
    502
    
                                                    
                                                
    503
        function invariant_seniorShareOpen() external view returns (bool) {
    504
            if (tranchePool.getPoolState() == TranchePool.PoolState.OPEN) {
    505
                return tranchePool.getTotalSeniorShares() == tranchePool.getSeniorTrancheIdleValue();
    506
            }
    507
            return true;
    508
        }
    509
    
                                                    
                                                
    510
        function invariant_loanState() external view returns (bool) {
    511
            uint256 nextId = loanEngine.getNextLoanId();
    512
            for (uint256 i = 1; i < nextId; i++) {
    513
                LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i);
    514
                
    515
                if (loan.state == LoanEngine.LoanState.NONE || 
    516
                    loan.state == LoanEngine.LoanState.CREATED) {
    517
                    if (loan.principalOutstanding != 0) return false;
    518
                }
    519
                
    520
                if (loan.state == LoanEngine.LoanState.REPAID ||
    521
                    loan.state == LoanEngine.LoanState.WRITTEN_OFF) {
    522
                    if (loan.principalOutstanding != 0) return false;
    523
                }
    524
                
    525
                if (loan.state == LoanEngine.LoanState.ACTIVE) {
    526
                    if (loan.principalOutstanding > loan.principalIssued) return false;
    527
                }
    528
            }
    529
            return true;
    530
        }
    531
    
                                                    
                                                
    532
        function invariant_interestMonotonicity() external view returns (bool) {
    533
    ✓ 8.5K
            return tranchePool.getSeniorInterestIndex() >= 1e18 &&
    534
                   tranchePool.getJuniorInterestIndex() >= 1e18 &&
    535
                   tranchePool.getEquityInterestIndex() >= 1e18;
    536
        }
    537
    
                                                    
                                                
    538
        function invariant_poolState() external view returns (bool) {
    539
    ✓ 1.7K
            TranchePool.PoolState state = tranchePool.getPoolState();
    540
            if (state == TranchePool.PoolState.OPEN || 
    541
                state == TranchePool.PoolState.CLOSED) {
    542
                if (tranchePool.getTotalDeployedValue() != 0) return false;
    543
            }
    544
    ✓ 1.7K
            return true;
    545
        }
    546
    
                                                    
                                                
    547
        function invariant_interestAccounting() external view returns (bool) {
    548
            uint256 nextId = loanEngine.getNextLoanId();
    549
            for (uint256 i = 1; i < nextId; i++) {
    550
                LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i);
    551
                if (loan.state == LoanEngine.LoanState.REPAID) {
    552
                    if (loan.interestAccrued != 0) return false;
    553
                }
    554
                if (loan.state == LoanEngine.LoanState.WRITTEN_OFF) {
    555
                    if (loan.interestAccrued != 0) return false;
    556
                }
    557
            }
    558
            return true;
    559
        }
    560
        
    561
        // =========================================================================
    562
        // HELPER FUNCTIONS
    563
        // =========================================================================
    564
        
    565
        function _accrueInterest(uint256 loanId) internal view returns (uint256) {
    566
    ✓ 35.2K
            LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId);
    567
            
    568
    ✓ 35.2K
            uint256 timeElapsed = block.timestamp - loan.lastAccrualTimestamp;
    569
    ✓ 35.2K
            if (loan.principalOutstanding == 0) return 0;
    570
            
    571
    ✓ 35.2K
            uint256 interest = (loan.principalOutstanding * loan.aprBps * timeElapsed) / (365 days * 10_000);
    572
            return interest;
    573
        }
    574
        
    575
        function _createEligibilityCriteria() internal pure returns (CreditPolicy.EligibilityCriteria memory) {
    576
    ✓ 1
            return CreditPolicy.EligibilityCriteria({
    577
                minAnnualRevenue: 1_00_00_000,
    578
                minEBITDA: 10_00_000,
    579
                minTangibleNetWorth: 5_00_00_000,
    580
                minBusinessAgeDays: 180,
    581
                maxDefaultsLast36Months: 0,
    582
                bankruptcyExcluded: true
    583
            });
    584
        }
    585
        
    586
        function _createFinancialRatios() internal pure returns (CreditPolicy.FinancialRatios memory) {
    587
            return CreditPolicy.FinancialRatios({
    588
    ✓ 1
                maxTotalDebtToEBITDA: 4e18,
    589
    ✓ 1
                minInterestCoverageRatio: 2e18,
    590
                minCurrentRatio: 1e18,
    591
    ✓ 1
                minEBITDAMarginBps: 1500
    592
            });
    593
        }
    594
        
    595
        function _createConcentrationLimits() internal pure returns (CreditPolicy.ConcentrationLimits memory) {
    596
            return CreditPolicy.ConcentrationLimits({
    597
    ✓ 1
                maxSingleBorrowerBps: 1000,
    598
    ✓ 1
                maxIndustryConcentrationBps: 3000
    599
            });
    600
        }
    601
        
    602
        function _createAttestationRequirements() internal pure returns (CreditPolicy.AttestationRequirements memory) {
    603
            return CreditPolicy.AttestationRequirements({
    604
    ✓ 1
                maxAttestationAgeDays: 90,
    605
                reAttestationFrequencyDays: 180,
    606
                requiresCPAAttestation: true
    607
            });
    608
        }
    609
        
    610
        function _createMaintenanceCovenants() internal pure returns (CreditPolicy.MaintenanceCovenants memory) {
    611
    ✓ 1
            return CreditPolicy.MaintenanceCovenants({
    612
                maxLeverageRatio: 4e18,
    613
                minCoverageRatio: 2e18,
    614
                minLiquidityAmount: 1_00_00_000,
    615
                allowsDividends: false,
    616
                reportingFrequencyDays: 90
    617
            });
    618
        }
    619
        
    620
        function _createMockTier(string memory name) internal pure returns (CreditPolicy.LoanTier memory) {
    621
            return CreditPolicy.LoanTier({
    622
                name: name,
    623
                minRevenue: 1_00_00_000,
    624
                maxRevenue: 5_00_00_000,
    625
                minEBITDA: 10_00_000,
    626
    ✓ 1
                maxDebtToEBITDA: 3e18,
    627
                maxLoanToEBITDA: 2e18,
    628
    ✓ 1
                interestRateBps: 800,
    629
                originationFeeBps: 100,
    630
    ✓ 1
                termDays: 365,
    631
                active: true
    632
            });
    633
        }
    634
        
    635
        function _bound(uint256 x, uint256 min, uint256 max) internal pure returns (uint256) {
    636
    ✓ 359.2K
            if (min > max) return min;
    637
    ✓ 359.2K
            if (x < min) return min;
    638
    ✓ 298.3K
            if (x > max) return max;
    639
    ✓ 9.7K
            return min + (x % (max - min + 1));
    640
        }
    641
        
    642
        function _min(uint256 a, uint256 b) internal pure returns (uint256) {
    643
    ✓ 82.9K
            return a < b ? a : b;
    644
        }
    645
        
    646
        // Medusa impersonate pattern
    647
        function _impersonate(address who) internal {
    648
    ✓ 84.3K
            vm.startPrank(who);
    649
        }
    650
        
    651
        function _stopImpersonate() internal {
    652
    ✓ 84.3K
            vm.stopPrank();
    653
        }
    654
    }
    655
    
                                                    
                                                
    656
    // Hevm interface for cheatcodes
    657
    interface Hevm {
    658
        function startPrank(address) external;
    659
        function stopPrank() external;
    660
        function warp(uint256) external;
    661
    }
    100.0% test/mocks/MockLoanProofVerifier.sol
    Lines covered: 2 / 2 (100.0%)
    1
    // SPDX-License-Identifier: MIT
    2
    pragma solidity ^0.8.24;
    3
    
                                                    
                                                
    4
    ✓ 1
    contract MockLoanProofVerifier {
    5
        function verify(
    6
            bytes calldata proof,
    7
            bytes32[] calldata publicInputs
    8
        ) external pure returns (bool) {
    9
    ✓ 82.9K
            return true;
    10
        }
    11
    }
    12